├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── client │ ├── auth.test.ts │ ├── auth.ts │ ├── client.test.ts │ ├── client.ts │ ├── dc.test.ts │ ├── dc.ts │ ├── index.ts │ ├── rpc.test.ts │ ├── rpc.ts │ ├── types.ts │ ├── updates.test.ts │ └── updates.ts ├── crypto │ ├── pq.test.ts │ ├── pq.ts │ ├── rsa │ │ ├── encrypt.test.ts │ │ ├── encrypt.ts │ │ ├── keys.test.ts │ │ └── keys.ts │ ├── srp.test.ts │ └── srp.ts ├── global.d.ts ├── index.ts ├── message │ ├── encrypted.test.ts │ ├── encrypted.ts │ ├── error.test.ts │ ├── error.ts │ ├── index.ts │ ├── message.test.ts │ ├── message.ts │ ├── message.v1.test.ts │ ├── message.v1.ts │ ├── plain.test.ts │ ├── plain.ts │ ├── resolve.test.ts │ └── resolve.ts ├── mock │ ├── auth_key.ts │ ├── client.ts │ ├── client_dc_service.ts │ ├── client_meta.ts │ ├── client_updates.ts │ ├── message_encrypted.ts │ ├── message_error.ts │ ├── message_plain.ts │ ├── srp.ts │ └── transport_config.ts ├── serialization │ ├── index.ts │ ├── reader.test.ts │ ├── reader.ts │ ├── utils.test.ts │ ├── utils.ts │ ├── writer.test.ts │ └── writer.ts ├── tl │ ├── index.ts │ ├── layer113 │ │ ├── builder.ts │ │ ├── parser.ts │ │ └── types.ts │ └── mtproto │ │ ├── builder.ts │ │ ├── parser.ts │ │ └── types.ts ├── transport │ ├── abstract.test.ts │ ├── abstract.ts │ ├── http.test.ts │ ├── http.ts │ ├── index.ts │ ├── protocol │ │ ├── index.ts │ │ ├── intermediate.test.ts │ │ ├── intermediate.ts │ │ ├── obfuscation.test.ts │ │ └── obfuscation.ts │ ├── socket.test.ts │ └── socket.ts └── utils │ ├── crc32.test.ts │ ├── crc32.ts │ └── log.ts ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest/globals": true 6 | }, 7 | "extends": [ 8 | "airbnb-typescript" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint", 21 | "jest" 22 | ], 23 | "rules": { 24 | "max-len": [2, 160, 4], 25 | "no-continue": 0, 26 | "no-bitwise": 0, 27 | "no-underscore-dangle": "off", 28 | "import/no-webpack-loader-syntax": "off", 29 | "prefer-template": "off", 30 | "no-param-reassign": "off", 31 | "import/prefer-default-export": "off", 32 | "class-methods-use-this": "off", 33 | "@typescript-eslint/no-unused-vars": "off", 34 | "lines-between-class-members": "off", 35 | "no-dupe-class-members": "off", 36 | "no-plusplus": "off", 37 | "object-curly-newline": "off", 38 | "@typescript-eslint/camelcase": "off", 39 | "@typescript-eslint/no-unused-expressions": "off" 40 | }, 41 | "settings": { 42 | "import/resolver": { 43 | "webpack": {} 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | dist/ 5 | coverage/ 6 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mtproto-js 2 | Extremly fast and lightweight JavaScript MTProto 2.0 implementation. Current supported API layer is 113. 3 | Works both for browser and Node. 4 | Reference: https://core.telegram.org/mtproto 5 | 6 | Created for Telegram JS Contest 2020: 7 | https://contest.com/javascript-web-3/entry1422 8 | 9 | ## General Use 10 | ```ts 11 | import { Client } from 'mtproto-js'; 12 | 13 | /** Client configuration */ 14 | type ClientConfig = { 15 | test: boolean, 16 | debug: boolean, 17 | ssl: boolean, 18 | dc: number, 19 | transport: 'websocket' | 'http', 20 | meta: Record, 21 | 22 | APILayer: number, 23 | APIID?: string, 24 | APIHash?: string, 25 | 26 | deviceModel: string, 27 | systemVersion: string, 28 | appVersion: string, 29 | langCode: string, 30 | 31 | autoConnect: boolean, 32 | }; 33 | 34 | const client = new Client(clientConfig); 35 | 36 | client.on('metaChanged', (newMeta) => console.log(newMeta)); // save keys and authorization data 37 | client.updates.on((update) => console.log(update)); 38 | client.updates.fetch(); 39 | 40 | client.call( 41 | 'help.getNearestDc', // method name 42 | {}, // params 43 | { dc: 2, thread: 1, transport: 'websocket' }, // headers (optional) 44 | (err, result) => console.log(err, result), 45 | ); 46 | ``` 47 | 48 | ## Features 49 | * Unit tests 50 | * TL schema code generation 51 | * Typed 52 | * Multithreading 53 | * Supports both WebSockets and HTTP 54 | 55 | ## Dependencies 56 | * [[cryptography aes, pbkdf2, sha1, sha256, sha512](https://github.com/spalt08/cryptography)] 57 | * [[big-integer](https://github.com/peterolson/BigInteger.js/)] 58 | * [[pako](https://github.com/nodeca/pako)] 59 | 60 | ## Contributions 61 | Thanks @misupov for schema code generation tool https://github.com/misupov/tg-schema-generator -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | '/src', 4 | ], 5 | testMatch: [ 6 | '**/__tests__/**/*.+(ts|js)', 7 | '**/?(*.)+(spec|test).+(ts|js)', 8 | ], 9 | preset: 'ts-jest', 10 | testPathIgnorePatterns: ['/dist/', '/node_modules/', '/src/tl/layer113/'], 11 | coveragePathIgnorePatterns: ['/src/tl/layer113/'], 12 | moduleFileExtensions: ['ts', 'js', 'json'], 13 | collectCoverage: true, 14 | globals: { 15 | 'ts-jest': { 16 | tsConfig: { 17 | target: 'es2020', 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mtproto-js", 3 | "description": "MTProto implementation with pure JavaScript", 4 | "version": "0.2.0", 5 | "author": "Konstantin Darutkin", 6 | "scripts": { 7 | "prepare": "yarn run build", 8 | "lint": "eslint --ext .ts ./src", 9 | "build": "rm -rf dist/* && rollup -c", 10 | "analyze": "yarn run build -- --analyze", 11 | "test": "jest", 12 | "publish": "yarn build" 13 | }, 14 | "dependencies": { 15 | "@cryptography/aes": "^0.1.1", 16 | "@cryptography/pbkdf2": "^0.1.2", 17 | "@cryptography/sha1": "^0.2.0", 18 | "@cryptography/sha256": "^0.2.0", 19 | "@cryptography/sha512": "^0.2.0", 20 | "big-integer": "^1.6.46", 21 | "pako": "^1.0.10" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-node-resolve": "^7.1.1", 25 | "@rollup/plugin-typescript": "^4.0.0", 26 | "@types/jest": "^24.0.22", 27 | "@types/pako": "^1.0.1", 28 | "@typescript-eslint/eslint-plugin": "^2.6.1", 29 | "@typescript-eslint/parser": "^2.6.1", 30 | "babel-eslint": "^10.0.3", 31 | "babel-loader": "^8.0.6", 32 | "cross-env": "^6.0.3", 33 | "eslint": "^6.6.0", 34 | "eslint-config-airbnb-typescript": "^6.0.0", 35 | "eslint-import-resolver-webpack": "^0.11.1", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-jest": "^23.0.2", 38 | "eslint-plugin-jsx-a11y": "^6.2.3", 39 | "eslint-plugin-react": "^7.16.0", 40 | "flow-bin": "^0.109.0", 41 | "jest": "^25.3.0", 42 | "jest-environment-node": "^25.3.0", 43 | "rimraf": "^3.0.1", 44 | "rollup": "^2.2.0", 45 | "rollup-plugin-terser": "^5.3.0", 46 | "terser-webpack-plugin": "^2.1.2", 47 | "text-encoding": "^0.7.0", 48 | "ts-jest": "^25.3.1", 49 | "ts-loader": "^6.2.1", 50 | "typescript": "^3.7.2" 51 | }, 52 | "license": "GPL-3.0-or-later", 53 | "main": "dist/cjs/mtproto-js.min.js", 54 | "module": "dist/es/mtproto-js.js", 55 | "types": "dist/typings/index.d.ts", 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/spalt08/mtproto-js.git" 59 | }, 60 | "lint-staged": { 61 | "*.{js,ts}": [ 62 | "eslint" 63 | ] 64 | }, 65 | "keywords": [ 66 | "mtproto", 67 | "mt", 68 | "telegram", 69 | "protocol", 70 | "type-language" 71 | ], 72 | "bugs": { 73 | "url": "https://github.com/spalt08/mtproto-js/issues" 74 | }, 75 | "homepage": "https://github.com/spalt08/mtproto-js#readme", 76 | "files": [ 77 | "dist/*", 78 | "package.json" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | import typescript from '@rollup/plugin-typescript'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const packageName = 'mtproto-js'; 8 | const entryFile = 'src/index.ts'; 9 | const formats = ['cjs', 'umd', 'es']; 10 | const inlineDynamicImports = false; 11 | 12 | export default [ 13 | // js only 14 | { 15 | input: entryFile, 16 | inlineDynamicImports, 17 | output: [ 18 | ...formats.map((format) => ({ 19 | name: packageName, 20 | file: `dist/${format}/${packageName}.js`, 21 | format, 22 | })), 23 | ...formats.map((format) => ({ 24 | name: packageName, 25 | file: `dist/${format}/${packageName}.min.js`, 26 | format, 27 | plugins: [terser()], 28 | })), 29 | ], 30 | plugins: [ 31 | typescript({ 32 | typescript: require('typescript'), 33 | include: ['src/**/*'], 34 | exclude: ['*.test.ts'], 35 | }), 36 | resolve({ 37 | jsnext: true, 38 | main: false, 39 | }), 40 | ], 41 | }, 42 | 43 | // declaration 44 | { 45 | input: entryFile, 46 | output: { 47 | dir: 'dist/typings', 48 | }, 49 | plugins: [ 50 | typescript({ 51 | emitDeclarationOnly: true, 52 | declaration: true, 53 | incremental: false, 54 | outDir: 'dist/typings', 55 | target: 'es5', 56 | rootDir: 'src', 57 | exclude: ['*.test.ts'], 58 | include: ['src/**/*'], 59 | }), 60 | ], 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /src/client/auth.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { 3 | KeyExchangeContext, createCipher, createDHRequestParams, createClientDHParams, createAuthKey, initConnection, 4 | bindTempAuthKey, createBindingEncryptedPayload, 5 | } from './auth'; 6 | import configMock from '../mock/transport_config'; 7 | import { ab2i, i2ab } from '../serialization'; 8 | import Client from './client'; 9 | import { AuthKey } from './types'; 10 | import metaMock from '../mock/client_meta'; 11 | 12 | test('auth | create cipher', () => { 13 | const ctx: KeyExchangeContext = { 14 | nonce: new Uint32Array([0x4c546e5e, 0xa5c1d7a4, 0xd8956ac4, 0xe66af237]), 15 | newNonce: new Uint32Array([0xa0730a3d, 0x662846a9, 0x9d23ab44, 0x12ec51bc, 0x1cd1df99, 0x2b57682d, 0x20985d03, 0xa918d01c]), 16 | serverNonce: new Uint32Array([0xf3a65684, 0x619b7647, 0xccfd1d95, 0xe55e217e]), 17 | expiresAfter: 0, 18 | }; 19 | 20 | const cipher = createCipher(ctx); 21 | 22 | expect(cipher.key).toEqual(new Uint32Array([0x57fbad93, 0x1acab855, 0x5e7ca1ca, 0x0f9d1948, 0xdf570aa2, 0x8b8bdd0c, 0x35a1a328, 0xbd150f34])); 23 | expect(cipher.iv).toEqual(new Uint32Array([0x21e0c2d9, 0x8c201856, 0xa0515762, 0xf79a4308, 0x45dc877b, 0x0d166ace, 0x42e70c22, 0xa0730a3d])); 24 | }); 25 | 26 | test('auth | createDHRequestParams', () => { 27 | const random = new Uint32Array([ 28 | 0x2257f00e, 0x937d1ddf, 0x217c93a5, 0x4af8bad1, 0x1c8adfcb, 0xf5436dcd, 0xd1fc5493, 0xa878d287, 0x573baffd, 0xb8b67524, 0xee1a4c69, 0x5a4a8751, 29 | 0x45dcc1a1, 0x7a8104fe, 0x6ab7c815, 0x2a39dd73, 0x601d5f30, 0x1e4d808b, 0xf9dcaa62, 0x74a43eb7, 0xad545635, 0x641f7ba7, 0x4d1cba59, 0x2c6ee4ee, 30 | 0x621f71f6, 0x03af1024, 0x3297566a, 0x831aad13, 0x4108ef71, 0x08470485, 0x6473947f, 0x632bf5fb, 0xc4b3226a, 0xcb5efd67, 0xf1ada600, 31 | ]); 32 | 33 | const ctx: KeyExchangeContext = { 34 | nonce: new Uint32Array([0x1d4fd969, 0x64dbf837, 0xb96629f5, 0xa936aa1d]), 35 | newNonce: new Uint32Array([0x13ec88e4, 0xe4a680b5, 0x163f0438, 0x262f19c5, 0xc85ae6f2, 0x48eed49a, 0x41c3057a, 0x087934a7]), 36 | serverNonce: new Uint32Array([0xbca5cbe1, 0x8b7ff272, 0x0d8a8f4d, 0x9efd6b03]), 37 | expiresAfter: 0, 38 | pq: new Uint8Array([0x1d, 0x08, 0x38, 0x32, 0xf9, 0x7b, 0x6e, 0x49]), 39 | fingerprints: ['9692106da14b9f02', 'c3b42b026ce86b21'], 40 | }; 41 | 42 | const dhReq = createDHRequestParams(ctx, random); 43 | 44 | expect(dhReq.nonce).toEqual(ctx.nonce); 45 | expect(dhReq.p).toEqual(new Uint8Array([0x43, 0x82, 0x25, 0xe5])); 46 | expect(dhReq.q).toEqual(new Uint8Array([0x6e, 0x17, 0xe0, 0x95])); 47 | expect(dhReq.public_key_fingerprint).toEqual('c3b42b026ce86b21'); 48 | expect(dhReq.encrypted_data).toEqual( 49 | new Uint8Array([ 50 | 0x90, 0xbc, 0xab, 0xf1, 0x23, 0x6c, 0xce, 0xcd, 0xbd, 0x1d, 0xc8, 0x2b, 0x7e, 0xf2, 0x0f, 0xfb, 0x48, 0xf9, 0xc1, 0xc2, 0xf8, 0x7d, 0x41, 51 | 0x1f, 0xba, 0xf7, 0x08, 0xc2, 0xf7, 0xc6, 0xf3, 0x3a, 0xf0, 0x3e, 0x7a, 0x62, 0xb0, 0x5b, 0x92, 0xad, 0xdb, 0xfd, 0x2d, 0x00, 0xe3, 0x01, 0x2b, 52 | 0x16, 0x64, 0x32, 0x67, 0x2e, 0x99, 0xd6, 0xe2, 0x8a, 0x5c, 0xfa, 0x1f, 0x94, 0x18, 0xe9, 0xdc, 0xb5, 0x87, 0x1a, 0xfa, 0x91, 0xb8, 0x42, 0x58, 53 | 0xda, 0x43, 0x43, 0xe3, 0xfb, 0x59, 0xfd, 0x2a, 0xd1, 0x92, 0xaf, 0x16, 0x89, 0x72, 0x43, 0x81, 0xba, 0x7d, 0x1d, 0xa4, 0x90, 0x66, 0xd9, 0x52, 54 | 0x60, 0xe2, 0x44, 0x79, 0x7a, 0x2d, 0x01, 0x6d, 0x2a, 0xe8, 0xe9, 0x19, 0xbd, 0xaf, 0xe8, 0x12, 0x22, 0x3e, 0x03, 0x2d, 0xe7, 0xf2, 0x32, 0x7c, 55 | 0x2c, 0x50, 0x63, 0x69, 0xee, 0xf7, 0xd6, 0xfc, 0x04, 0x54, 0xc2, 0xde, 0x33, 0x9e, 0x9a, 0x45, 0x6d, 0x3e, 0x75, 0x9b, 0x28, 0x9b, 0xee, 0x2f, 56 | 0x02, 0x09, 0x1d, 0xae, 0x08, 0xfc, 0x6e, 0x55, 0x1b, 0x60, 0x28, 0xc5, 0xfb, 0x17, 0x2b, 0xb8, 0x3a, 0x45, 0x53, 0x66, 0x9b, 0x61, 0xca, 0x62, 57 | 0xa3, 0xa8, 0xf6, 0xac, 0xad, 0x0a, 0x5e, 0x39, 0xf3, 0x87, 0x57, 0xad, 0x53, 0x91, 0xe2, 0x07, 0x1f, 0xe6, 0x76, 0x74, 0x5b, 0x64, 0x31, 0x93, 58 | 0xc4, 0xad, 0x91, 0xfe, 0x51, 0x9b, 0x7b, 0x53, 0xc7, 0x39, 0xb4, 0x80, 0xd2, 0xb4, 0xf7, 0x67, 0xd8, 0x61, 0xca, 0xd0, 0x12, 0xd9, 0x05, 0x31, 59 | 0xf8, 0xfd, 0x5f, 0x32, 0x57, 0xb1, 0x13, 0x66, 0x3c, 0xde, 0xe1, 0xb0, 0x06, 0x01, 0xa9, 0xdc, 0x06, 0x2e, 0x62, 0x76, 0x87, 0x0d, 0x43, 0x69, 60 | 0xcb, 0xe3, 0xa3, 0xdb, 0x66, 0x51, 0x8d, 0x41, 0x24, 0x91, 0x0a, 0x49, 0xe1, 0xb8, 0x47, 0xd3, 61 | ]).buffer, 62 | ); 63 | }); 64 | 65 | 66 | test('auth | createClientDHParams', () => { 67 | const ctx: KeyExchangeContext = { 68 | nonce: new Uint32Array([0x33bfa240, 0xef1f3fae, 0x73a2b97b, 0x916ed586]), 69 | newNonce: new Uint32Array([0x44eafbbb, 0xa0822b8d, 0x07e3c786, 0xe789f9ca, 0x5b86730c, 0x0553feaf, 0x9a0a1665, 0x0eb549b8]), 70 | serverNonce: new Uint32Array([0xc161eb5a, 0x0fe4b4c5, 0xd9c4f53b, 0x2e483522]), 71 | expiresAfter: 0, 72 | fingerprints: ['9692106da14b9f02', 'c3b42b026ce86b21'], 73 | g: 3, 74 | ga: new Uint8Array([ 75 | 0x8f, 0x7d, 0x43, 0xdc, 0xb9, 0xa0, 0xf0, 0x7a, 0x6d, 0x96, 0x63, 0xc7, 0xe0, 0x91, 0x5b, 0xd7, 0x4e, 0x43, 0xc2, 0x57, 0xec, 0xb6, 0x19, 0x97, 76 | 0x81, 0x42, 0x04, 0x9f, 0x8b, 0x3f, 0x59, 0xd9, 0x67, 0xd0, 0x4f, 0xe8, 0x61, 0x7a, 0xd8, 0xa4, 0x55, 0xb6, 0xfd, 0x12, 0x73, 0x4c, 0x57, 0xaf, 77 | 0x60, 0x4b, 0x8c, 0x05, 0xa0, 0xa6, 0x54, 0x5f, 0x84, 0xb8, 0x8b, 0x4c, 0x98, 0xae, 0x38, 0x8a, 0x02, 0xfb, 0xc0, 0x99, 0x31, 0x63, 0x05, 0x6d, 78 | 0xdc, 0xdf, 0x4a, 0xb3, 0x92, 0x37, 0x8a, 0x0c, 0xb8, 0x41, 0xc2, 0x0f, 0x73, 0x00, 0xdd, 0x88, 0x53, 0xc8, 0x0b, 0x1f, 0x1f, 0x1a, 0x9f, 0x9f, 79 | 0xe4, 0x05, 0x49, 0xda, 0xcd, 0x77, 0x0d, 0x10, 0xf0, 0xb2, 0x2c, 0xbe, 0xe4, 0x0b, 0xc6, 0x13, 0x1a, 0x8a, 0xc9, 0x3a, 0x95, 0xaa, 0xee, 0xc6, 80 | 0x37, 0x03, 0x69, 0xab, 0xea, 0xe7, 0xa5, 0x8c, 0x39, 0x29, 0x8e, 0xeb, 0x8f, 0x88, 0x3d, 0x18, 0x5c, 0x9c, 0xcd, 0xbf, 0x44, 0x9e, 0x21, 0x47, 81 | 0x0d, 0x9f, 0x99, 0x39, 0xdd, 0x99, 0xbf, 0x04, 0x84, 0x0e, 0x65, 0x19, 0x55, 0xcf, 0x32, 0xbd, 0x30, 0x07, 0x4f, 0x16, 0x7a, 0xe8, 0x00, 0xad, 82 | 0x04, 0x51, 0x60, 0xfc, 0xf1, 0xbd, 0x5e, 0xae, 0x63, 0x4a, 0x25, 0x6a, 0xc4, 0xa0, 0xa3, 0x47, 0xe8, 0x32, 0x2d, 0x52, 0xa2, 0x6b, 0x15, 0x29, 83 | 0x95, 0xfa, 0x9a, 0x41, 0x12, 0x7e, 0x1f, 0x56, 0xf7, 0x4b, 0x7e, 0xe0, 0x1f, 0x3d, 0x3e, 0x98, 0x62, 0x65, 0xd2, 0xb0, 0xfc, 0x62, 0x45, 0xb5, 84 | 0xbf, 0x2a, 0x7b, 0x99, 0xb8, 0x09, 0x68, 0x8e, 0x93, 0x2b, 0x5e, 0xd5, 0xa3, 0x41, 0x3d, 0x9b, 0xe8, 0x71, 0xfc, 0xf9, 0x42, 0xd4, 0xc4, 0x16, 85 | 0x31, 0x5a, 0x4b, 0xb8, 0x77, 0x40, 0x01, 0xa7, 0xbf, 0x66, 0x6e, 0xeb, 0x82, 0xe4, 0x61, 0xa6, 86 | ]), 87 | dh: new Uint8Array([ 88 | 0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x04, 0x8e, 0x6c, 0x52, 0x2f, 0x70, 0xf1, 0x3f, 0x73, 0x98, 0x0d, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1, 89 | 0x49, 0x34, 0xd0, 0x37, 0x56, 0x3d, 0x93, 0x0f, 0x48, 0x19, 0x8a, 0x0a, 0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb, 90 | 0xfa, 0x33, 0x6f, 0x6e, 0x0a, 0xc9, 0x25, 0x13, 0x95, 0x43, 0xae, 0xd4, 0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, 0x70, 0x5a, 91 | 0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64, 92 | 0x24, 0x77, 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a, 0xc8, 0xcc, 0x49, 0x88, 0x07, 0x08, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f, 93 | 0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, 0x6b, 0x0f, 0x6b, 0x41, 94 | 0x0d, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, 0x4e, 0x0e, 0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0x0d, 0x59, 0x65, 0xb4, 95 | 0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4, 0x65, 0xb0, 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4, 96 | 0xe4, 0x18, 0xfc, 0x15, 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff, 0x5e, 0xed, 0x70, 0x05, 0x0d, 0xed, 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59, 97 | 0xd9, 0x56, 0x85, 0x0c, 0xe9, 0x29, 0x85, 0x1f, 0x0d, 0x81, 0x15, 0xf6, 0x35, 0xb1, 0x05, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, 0x54, 0xbf, 98 | 0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x04, 0x03, 0x11, 0x9c, 0xd8, 0xe3, 0xb9, 0x2f, 0xcc, 0x5b, 99 | ]), 100 | }; 101 | 102 | const b = new Uint8Array([ 103 | 0x54, 0x8c, 0x22, 0x57, 0x08, 0x33, 0x22, 0xb1, 0x43, 0x7a, 0x85, 0x73, 0x50, 0xdd, 0xd5, 0xc4, 0x31, 0x1e, 0xbf, 0x7f, 0x06, 0xc2, 0x4c, 0x45, 104 | 0x66, 0xfd, 0xc6, 0xf0, 0x3b, 0xe6, 0xb4, 0x4a, 0xb6, 0x41, 0xb1, 0x81, 0xe3, 0xcc, 0xc9, 0x80, 0x89, 0x7b, 0x47, 0x62, 0xe8, 0x8d, 0xdf, 0x98, 105 | 0x47, 0x8b, 0x93, 0x8a, 0x68, 0xdc, 0x6e, 0x9a, 0x3e, 0x83, 0x4e, 0x4b, 0x80, 0xca, 0xab, 0x01, 0x27, 0xd0, 0x4f, 0x54, 0xba, 0x5e, 0x14, 0x93, 106 | 0x2a, 0xa0, 0x59, 0x7c, 0xc3, 0x55, 0xfd, 0x88, 0xcf, 0x6b, 0x54, 0xf1, 0xa3, 0x46, 0x39, 0x33, 0x80, 0x86, 0x4a, 0x26, 0x04, 0x28, 0x0e, 0x8e, 107 | 0xab, 0xb8, 0x13, 0xcf, 0xf7, 0x06, 0x17, 0xf1, 0x0f, 0x5e, 0x79, 0x97, 0x04, 0xc1, 0x62, 0x05, 0x73, 0x68, 0x7c, 0xd9, 0x36, 0xdb, 0x7a, 0xe6, 108 | 0xb8, 0x2b, 0x49, 0x03, 0x93, 0x67, 0x4f, 0x42, 0x7e, 0x2c, 0xe6, 0x59, 0xe2, 0x50, 0xe7, 0x6f, 0xa9, 0xcd, 0x6a, 0x22, 0xf2, 0x99, 0xac, 0x97, 109 | 0x63, 0x6c, 0x70, 0x3c, 0x83, 0x83, 0x28, 0x93, 0xe9, 0x05, 0x77, 0xcf, 0x76, 0xc9, 0x4f, 0x8a, 0x5a, 0x99, 0xc5, 0x1e, 0x96, 0xdf, 0x5e, 0xf2, 110 | 0xae, 0x40, 0x9a, 0xb9, 0xf8, 0xf1, 0xcd, 0xfa, 0x9b, 0x0c, 0xb3, 0xf2, 0x0c, 0xd8, 0xb5, 0x80, 0xaf, 0x78, 0x94, 0x4c, 0x84, 0x52, 0x20, 0xbc, 111 | 0xf1, 0x43, 0xac, 0x28, 0x24, 0xa9, 0x3f, 0x67, 0x9f, 0x63, 0xbc, 0xcc, 0xf4, 0xb3, 0xa2, 0xcb, 0xf9, 0x26, 0x7d, 0xa2, 0x28, 0x52, 0x5f, 0x01, 112 | 0x5c, 0x42, 0x2f, 0x3b, 0x2b, 0x86, 0x08, 0x12, 0xee, 0x24, 0xdb, 0x1e, 0xc4, 0x76, 0x6f, 0xbe, 0x13, 0xaf, 0x79, 0x15, 0xc3, 0xee, 0x9e, 0xeb, 113 | 0xc6, 0xa1, 0x58, 0x75, 0xf3, 0x72, 0xae, 0x5a, 0x5b, 0xdb, 0x59, 0x58, 0x7e, 0x26, 0x9f, 114 | ]); 115 | 116 | const padding = new Uint32Array([0x6861c04b, 0xfb2decf8, 0xa5a80dcd]); 117 | 118 | ctx.cipher = createCipher(ctx); 119 | 120 | const clientDH = createClientDHParams(ctx, b, padding); 121 | 122 | expect(clientDH.nonce).toEqual(ctx.nonce); 123 | expect(clientDH.server_nonce).toEqual(ctx.serverNonce); 124 | expect(ab2i(clientDH.encrypted_data)).toEqual( 125 | new Uint32Array([ 126 | 0x8f981940, 0xcb22be5a, 0xcddf9bae, 0x91cde160, 0x1a526bfd, 0xc6b9f640, 0x62f304d7, 0x14821c16, 0xd181bd4f, 0xb5523748, 0x2347542a, 0xd609c198, 127 | 0x6ec33e8d, 0xbb494e95, 0xf56a8b0c, 0x7b2e34d4, 0x566c4fb9, 0xefac9ecf, 0xdc5ecd97, 0x68b08f96, 0xf3812d21, 0x4ad664f7, 0x19ff61b0, 0x03ca944b, 128 | 0x6d1771f2, 0x587132e3, 0xa47c3ad9, 0x464275e4, 0x67d2a6d1, 0x5e0d8411, 0xb888007f, 0xf7a22ba3, 0xd40fe08a, 0x583cbd94, 0x17c11399, 0x8c38bde9, 129 | 0x84a0315e, 0x52554018, 0xa479320f, 0x04f5b003, 0x3990d082, 0xa9402437, 0x6b42cf74, 0xcd27fd6f, 0x0605d89f, 0x3a0e2719, 0xd4caee3c, 0xda132b59, 130 | 0x14faeee2, 0x90bca15a, 0xcc42491d, 0xae6ceece, 0xe934a795, 0xd17545ab, 0x19066282, 0xb6a59c7a, 0xc5ff09b9, 0xaad31cf6, 0x464b2e98, 0x9e99b0bb, 131 | 0x19aeee8d, 0x20e59c0d, 0x9c112d41, 0x3800369e, 0xb8616de4, 0x0cc2703d, 0x8a926dfd, 0x9ff741c7, 0x07a35120, 0xf79c3a91, 0x7805bcf4, 0x55be143e, 132 | 0x364d3c04, 0x5b10f48a, 0x5c66be3a, 0xf9a0d6b5, 0x16fe5e05, 0x61ad0e95, 0x3af1efb6, 0xaed9a90b, 0xfc5090f6, 0x2e49aa8f, 0x4a63ef7e, 0x89ce2887, 133 | ]), 134 | ); 135 | 136 | expect(ctx.key).toEqual( 137 | new Uint32Array([ 138 | 0x930c1741, 0x9da0f960, 0xd92af8d6, 0x5acb5cd3, 0xd8f2beab, 0x9c62c867, 0xd348afaa, 0x34de6a52, 0xd696fc45, 0x5ca65d7d, 0xc1d1ce40, 0x438e0446, 139 | 0x9b92fd53, 0x01b90495, 0x6252c953, 0x51307148, 0x5947d5dd, 0xad2bb050, 0xabcd8ced, 0x0126eb61, 0x90bd8f23, 0xc046270c, 0xf483e07e, 0x3ba2aa3a, 140 | 0x866a1fb0, 0x84464ec3, 0xe39cfe09, 0x8fbc1052, 0x6656248f, 0xb5a1f0da, 0x366e7998, 0xe149f867, 0x37149cbe, 0x50127887, 0xc3d64476, 0x216de9fb, 141 | 0x125cb738, 0x86661556, 0xbd9cf83d, 0x80fdcea3, 0x5f21be24, 0xd83301d2, 0x54480fa6, 0xb1f58bf2, 0x5d03181f, 0xc35891d9, 0x73073a56, 0xc0f5cece, 142 | 0x0a9749df, 0x978e072e, 0x4c8c2900, 0x93d56be0, 0x19fe403b, 0x7ba12a3e, 0xe87ac221, 0x14c3a1a5, 0xe133ed4b, 0xf3e5595d, 0xd44afa2a, 0xe2e380ac, 143 | 0xf63c93f3, 0xe63d556b, 0x4fc9e07b, 0x0310162c, 144 | ]), 145 | ); 146 | }); 147 | 148 | test('auth | createBindingEncryptedPayload', () => { 149 | const permKey = { 150 | id: 'ae73908158c55a02', 151 | key: new Uint32Array([ 152 | 0x6b65b83e, 0x739b5db5, 0x89b66bfd, 0xc6fc6925, 0xa5e8f5ef, 0x354086d2, 0xa533f0ba, 0xd4c4f68e, 0x82629098, 0xe5596726, 0x750a346b, 0xc9a3dbdd, 153 | 0x162fd3a6, 0xb0fdd6ec, 0x48a59516, 0xaef9f429, 0x4aa5751b, 0x4b6ee3b4, 0x2b07aa7a, 0x8b6c213e, 0x2b1ed6ec, 0xb9772a8b, 0xa5774b7f, 0x195c48e1, 154 | 0x78d67e5f, 0x912e0c43, 0x0a7406be, 0x0aafa2a1, 0xcc0a218d, 0xe81d7ebd, 0x861cc65d, 0xe7d05f94, 0xa0cd3bb3, 0xb4e7ce87, 0xa483c0d3, 0xde04a6b1, 155 | 0xc064488f, 0x75500dfb, 0xb11b5758, 0xa33fa5d1, 0xeba87db5, 0x809364ce, 0x21682543, 0xb414a176, 0xa9c82f46, 0x1bbc9a49, 0xa34464dd, 0x3122beb6, 156 | 0x84162ff2, 0x3f72c9ed, 0xf58e3023, 0x2cd0c4ee, 0xe0a6e843, 0xfbce7fe6, 0xa7902bd0, 0xbf32c86f, 0x203b2184, 0xe2588192, 0xc231cc54, 0x2a2794ed, 157 | 0xa471953a, 0xff941623, 0xc8a7fb85, 0x1cb59d6f, 158 | ]), 159 | }; 160 | 161 | const tempKey = { 162 | id: '513f796a3f2e348d', 163 | key: new Uint32Array([ 164 | 0xe326ac88, 0xa5a6ade5, 0x039daa84, 0x87022f44, 0x8ba7057b, 0x1f5ca416, 0x799c581a, 0xb0e2e270, 0x211de0f1, 0xc21cc94a, 0xbd28e949, 0x055c8720, 165 | 0xd412fa77, 0xa2af7939, 0xfd05da17, 0x69ada1bf, 0x07a424dd, 0x89aa8585, 0x549cda43, 0xe52665c3, 0xed3e956f, 0x21e2646e, 0x1a23fa6a, 0xe552c9db, 166 | 0x540e04eb, 0x80e7612e, 0x602eea54, 0xc7bc76bc, 0xa4a3510e, 0xd4dde340, 0xb0f47235, 0x62c7c64c, 0x7aa9a2be, 0xc95140db, 0xd0447e64, 0xd4f4fe6a, 167 | 0x32fa64d4, 0xb543400f, 0xdcffb6f9, 0xc685378a, 0xcc6a3862, 0x20b4ad3f, 0xef04f6e4, 0xc5dcf12e, 0x483ce06a, 0x69147430, 0xfee1458c, 0xb5b19e47, 168 | 0xfc91cd58, 0x2620beb1, 0x7d378bac, 0xc4944e1d, 0xa2ff4cb4, 0x7e0d37f7, 0xbda8ab69, 0xed3a21dc, 0x8bb25af3, 0xee1e8524, 0xdbb89ba3, 0x0b3ff097, 169 | 0x3b096ccc, 0xa7e8494e, 0x689ffef1, 0xa4e66f41, 170 | ]), 171 | expires: 1586617179, 172 | binded: false, 173 | }; 174 | 175 | const random = new Uint32Array([0x62ea5e03, 0x3752b489, 0x6b419502, 0x33f86792, 0x2ee19a5f, 0x76fe7921, 0x4a61f3b2, 0x71e1b4cf, 0x6286e493, 0x67d7f671]); 176 | 177 | const msgID = '5e91cd4b16b16b04'; 178 | 179 | expect(createBindingEncryptedPayload(permKey, tempKey, msgID, random).encrypted_message).toEqual( 180 | i2ab(new Uint32Array([ 181 | 0x025ac558, 0x819073ae, 0xa36e99ab, 0xf32ae054, 0x4cb833d1, 0x11865a20, 0x308cd877, 0x7d067ffb, 0x978dbdfd, 0x474fa019, 0xe288c5ce, 0x7954d4a4, 0x3a4c7042, 0xcd05df01, 0xac6a32ad, 0x868c5eed, 0xb01ccf62, 0xf742e418, 0xb9815b82, 0xea8cc203, 0x6f7a1431, 0x9089885a, 0x18ea8c93, 0x491d2015, 0xfecdda33, 0x83442c4d, 182 | ])), 183 | ); 184 | }); 185 | 186 | test('auth | create key', () => { 187 | const client = new Client({ 188 | test: true, 189 | dc: 2, 190 | autoConnect: false, 191 | meta: metaMock, 192 | debug: false, 193 | }); 194 | 195 | const async = new Promise((resolve, reject) => { 196 | createAuthKey(client, 2, 1, 0, (err, key) => { 197 | if (err) reject(err); 198 | else resolve(key); 199 | }); 200 | }); 201 | 202 | return async.then((key: AuthKey) => { 203 | if (!key) throw new Error('Key is nullable'); 204 | expect(key.id.length).toBe(16); 205 | expect(key.key.length).toBe(64); 206 | expect(key.id).not.toBe('0000000000000000'); 207 | }); 208 | }, 60000); 209 | 210 | test('Auth | binding and init session', () => { 211 | const client = new Client({ 212 | ...configMock, 213 | test: true, 214 | dc: 2, 215 | autoConnect: false, 216 | meta: metaMock, 217 | debug: false, 218 | }); 219 | 220 | const async = new Promise((resolve, reject) => { 221 | createAuthKey(client, 2, 1, 0, (err, k) => { 222 | if (err) reject(err); 223 | if (!k) { 224 | reject(new Error('Expected perm auth key')); 225 | return; 226 | } 227 | 228 | const permKey = k; 229 | 230 | createAuthKey(client, 2, 1, 3600 * 1, (errp, kp) => { 231 | if (errp) reject(errp); 232 | if (!kp) { 233 | reject(new Error('Expected temp auth key')); 234 | return; 235 | } 236 | 237 | const tempKey = kp; 238 | 239 | bindTempAuthKey(client, 2, permKey, tempKey, (res) => { 240 | if (!res) reject(new Error('Expected binding')); 241 | else initConnection(client, 2, resolve); 242 | }); 243 | }); 244 | }); 245 | }); 246 | 247 | return async.then((result) => { 248 | expect(result).toBeTruthy(); 249 | }); 250 | }, 150000); 251 | -------------------------------------------------------------------------------- /src/client/auth.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | import sha1 from '@cryptography/sha1'; 3 | import { IGE } from '@cryptography/aes'; 4 | import { getKeyByFingerprints } from '../crypto/rsa/keys'; 5 | import { logs } from '../utils/log'; 6 | import { randomize, i2h, i2ab, Reader32 } from '../serialization'; 7 | import { MessageV1, PlainMessage } from '../message'; 8 | import { BrentPrime } from '../crypto/pq'; 9 | import RSAEncrypt from '../crypto/rsa/encrypt'; 10 | import { ClientError, AuthKey, ClientInterface, CallHeaders, AuthKeyNotNull } from './types'; 11 | import { parse, build, Req_DH_params, Set_client_DH_params, Server_DH_inner_data, AuthBindTempAuthKey } from '../tl'; 12 | 13 | const BigInt = require('big-integer'); // fix for rollup 14 | 15 | const log = logs('auth'); 16 | 17 | export type KeyExchangeContext = { 18 | expiresAfter: number, 19 | nonce: Uint32Array, 20 | newNonce: Uint32Array, 21 | serverNonce?: Uint32Array, 22 | pq?: Uint8Array, 23 | p?: Uint8Array, 24 | q?: Uint8Array, 25 | fingerprints?: string[], 26 | fingerprint?: string, 27 | cipher?: IGE, 28 | g?: number, 29 | ga?: Uint8Array, 30 | dh?: Uint8Array, 31 | key?: Uint32Array, 32 | }; 33 | 34 | export type KeyExchangeCallback = (err: ClientError | null, key?: AuthKey) => void; 35 | 36 | /** 37 | * 38 | */ 39 | export function createCipher(ctx: KeyExchangeContext) { 40 | const sha1a = sha1.stream().update(ctx.newNonce).update(ctx.serverNonce!).digest(); 41 | const sha1b = sha1.stream().update(ctx.serverNonce!).update(ctx.newNonce).digest(); 42 | const sha1c = sha1.stream().update(ctx.newNonce).update(ctx.newNonce).digest(); 43 | 44 | const aesKey = new Uint32Array([ 45 | sha1a[0], sha1a[1], sha1a[2], sha1a[3], sha1a[4], sha1b[0], sha1b[1], sha1b[2], 46 | ]); 47 | 48 | const aesIv = new Uint32Array([ 49 | sha1b[3], sha1b[4], sha1c[0], sha1c[1], sha1c[2], sha1c[3], sha1c[4], ctx.newNonce[0], 50 | ]); 51 | 52 | return new IGE(aesKey, aesIv); 53 | } 54 | 55 | /** 56 | * 57 | */ 58 | export function createDHRequestParams(ctx: KeyExchangeContext, random?: Uint32Array): Req_DH_params { 59 | const [p, q] = BrentPrime(BigInt.fromArray(Array.from(ctx.pq!), 0x100)); 60 | const publicKey = getKeyByFingerprints(ctx.fingerprints!); 61 | 62 | ctx.p = new Uint8Array(p.toArray(0x100).value); 63 | ctx.q = new Uint8Array(q.toArray(0x100).value); 64 | 65 | // wrap p_q_inner_data 66 | const pqInner: Record = { 67 | _: 'p_q_inner_data', 68 | pq: ctx.pq, 69 | p: ctx.p, 70 | q: ctx.q, 71 | nonce: ctx.nonce, 72 | server_nonce: ctx.serverNonce, 73 | new_nonce: ctx.newNonce, 74 | }; 75 | 76 | // wrap p_q_inner_data_temp 77 | if (ctx.expiresAfter > 0) { 78 | pqInner._ = 'p_q_inner_data_temp'; 79 | pqInner.expires_in = ctx.expiresAfter; 80 | } 81 | 82 | // encrypt pq_inner_data 83 | const data = build(pqInner); 84 | const dataToEncrypt = new Uint32Array(64); 85 | const dataHash = sha1(data); 86 | 87 | if (!random) { 88 | random = new Uint32Array(59 - data.length); 89 | randomize(random); 90 | } 91 | 92 | for (let i = 0; i < 5; i++) dataToEncrypt[i] = dataHash[i]; 93 | for (let i = 0; i < data.length; i++) dataToEncrypt[5 + i] = data[i]; 94 | for (let i = 0; i < 59 - data.length; i++) dataToEncrypt[5 + data.length + i] = random[i]; 95 | 96 | const encrypted = RSAEncrypt(new Uint8Array(i2ab(dataToEncrypt)).slice(0, 255), publicKey.n, publicKey.e); 97 | 98 | ctx.fingerprint = publicKey.fingerprint; 99 | 100 | return { 101 | nonce: ctx.nonce, 102 | server_nonce: ctx.serverNonce!, 103 | p: ctx.p!, 104 | q: ctx.q!, 105 | public_key_fingerprint: ctx.fingerprint!, 106 | encrypted_data: i2ab(encrypted), 107 | }; 108 | } 109 | 110 | /** 111 | * 112 | */ 113 | export function createClientDHParams(ctx: KeyExchangeContext, rand?: Uint8Array, padding?: Uint32Array): Set_client_DH_params { 114 | if (!rand) { 115 | rand = new Uint8Array(20); 116 | randomize(rand); 117 | } 118 | 119 | // generate key; 120 | const g = BigInt(ctx.g!); 121 | const ga = BigInt.fromArray(Array.from(ctx.ga!), 0x100); 122 | const dhPrime = BigInt.fromArray(Array.from(ctx.dh!), 0x100); 123 | const b = BigInt.fromArray(Array.from(rand), 0x100); 124 | const gb = new Uint8Array(g.modPow(b, dhPrime).toArray(0x100).value); 125 | 126 | ctx.key = new Uint32Array(ga.modPow(b, dhPrime).toArray(0x100000000).value); 127 | 128 | // inner content for client_DH_inner_data 129 | const clientDH = build({ 130 | _: 'client_DH_inner_data', 131 | nonce: ctx.nonce, 132 | server_nonce: ctx.serverNonce, 133 | retry_id: '0000000000000000', 134 | g_b: gb, 135 | }); 136 | 137 | let len = 5 + clientDH.length; 138 | len += 4 - (len % 4); 139 | 140 | const plain = new Uint32Array(len); 141 | const dataHash = sha1(clientDH); 142 | 143 | if (!padding) { 144 | padding = new Uint32Array(len - 5 - clientDH.length); 145 | randomize(padding); 146 | } 147 | 148 | for (let i = 0; i < 5; i++) plain[i] = dataHash[i]; 149 | for (let i = 0; i < clientDH.length; i++) plain[5 + i] = clientDH[i]; 150 | for (let i = 0; i < padding.length; i++) plain[5 + clientDH.length + i] = padding[i]; 151 | 152 | // encrypt client_DH_inner_data 153 | const encryptedDH = ctx.cipher!.encrypt(plain); 154 | 155 | // params for set_client_DH_params 156 | return { 157 | nonce: ctx.nonce, 158 | server_nonce: ctx.serverNonce!, 159 | encrypted_data: i2ab(encryptedDH), 160 | }; 161 | } 162 | 163 | /** 164 | * Creates AuthKey using DH-exchange 165 | * Ref: https://core.telegram.org/mtproto/auth_key 166 | */ 167 | export function createAuthKey(client: ClientInterface, dc: number, thread: number, expiresAfter: number, cb: KeyExchangeCallback) { 168 | log(dc, `creating ${expiresAfter > 0 ? 'temporary' : 'permanent'} key`); 169 | 170 | const ctx: KeyExchangeContext = { 171 | nonce: new Uint32Array(4), 172 | newNonce: new Uint32Array(8), 173 | expiresAfter, 174 | }; 175 | 176 | randomize(ctx.nonce); 177 | randomize(ctx.newNonce); 178 | 179 | const headers: CallHeaders = { dc, thread, transport: 'websocket' }; 180 | 181 | /** 182 | * Auth Flow 183 | * Step 1. Send random nonce. 184 | * @mtproto req_pq_multi 185 | */ 186 | client.plainCall('req_pq_multi', { nonce: ctx.nonce }, headers, (err, resPQ) => { 187 | if (err || !resPQ || resPQ._ !== 'resPQ') { 188 | log(dc, 'Unexpected resPQ response'); 189 | cb(err); 190 | return; 191 | } 192 | 193 | ctx.serverNonce = resPQ.server_nonce; 194 | ctx.fingerprints = resPQ.server_public_key_fingerprints; 195 | ctx.pq = new Uint8Array(resPQ.pq.slice(0)); 196 | ctx.cipher = createCipher(ctx); 197 | 198 | /** 199 | * Auth Flow 200 | * Step 2. Request Diffie-Hellman params 201 | * @mtproto req_DH_params 202 | */ 203 | client.plainCall('req_DH_params', createDHRequestParams(ctx), headers, (errd, resDH) => { 204 | if (errd || !resDH || resDH._ !== 'server_DH_params_ok') { 205 | log(dc, thread, 'Unexpected req_DH_params response'); 206 | cb(errd); 207 | return; 208 | } 209 | 210 | // decrypt encrypted_answer 211 | const decryptedDH = ctx.cipher!.decrypt(new Uint8Array(resDH.encrypted_answer)); 212 | const serverDH = parse(new Reader32(decryptedDH.subarray(5))) as Server_DH_inner_data; 213 | 214 | if (!serverDH || serverDH._ !== 'server_DH_inner_data') { 215 | log(dc, thread, 'Unable to decrypt aes-256-ige'); 216 | cb({ type: 'internal', code: 0, message: 'Unable to decrypt aes-256-ige' }); 217 | return; 218 | } 219 | 220 | // todo: server time sync 221 | // todo: check dh prime, ga, gb 222 | ctx.g = serverDH.g; 223 | ctx.ga = new Uint8Array(serverDH.g_a); 224 | ctx.dh = new Uint8Array(serverDH.dh_prime); 225 | 226 | /** 227 | * Auth Flow 228 | * Step 3. Send Client Diffie-Hellman params 229 | * @mtproto set_client_DH_params 230 | */ 231 | client.plainCall('set_client_DH_params', createClientDHParams(ctx), headers, (errs, sDH) => { 232 | if (errs || !sDH || sDH._ !== 'dh_gen_ok') { 233 | log(dc, thread, 'Unexpected set_client_DH_params response'); 234 | cb(err); 235 | return; 236 | } 237 | 238 | const keyhash = new Reader32(sha1(ctx.key!), 3); 239 | const keyid = keyhash.long(); 240 | 241 | const authKey: AuthKey = { 242 | id: keyid, 243 | key: ctx.key!, 244 | }; 245 | 246 | if (expiresAfter > 0) { 247 | authKey.expires = Math.floor(Date.now() / 1000) + expiresAfter; 248 | authKey.binded = false; 249 | 250 | ctx.serverNonce![0] ^= ctx.newNonce[0]; 251 | ctx.serverNonce![1] ^= ctx.newNonce[1]; 252 | 253 | const saltReader = new Reader32(ctx.serverNonce!); 254 | client.dc.setSalt(dc, saltReader.long()); 255 | } 256 | 257 | if (expiresAfter > 0) client.dc.setTemporaryKey(dc, authKey); 258 | else client.dc.setPermanentKey(dc, authKey); 259 | 260 | log(dc, `${expiresAfter > 0 ? 'temporary' : 'permanent'} key created (thread: ${thread})`); 261 | 262 | if (cb) cb(null, authKey); 263 | }); 264 | }); 265 | }); 266 | } 267 | 268 | export function createBindingEncryptedPayload(permKey: AuthKeyNotNull, tempKey: AuthKeyNotNull, msgID: string, rand?: Uint32Array): AuthBindTempAuthKey { 269 | if (!rand) { 270 | rand = new Uint32Array(10); 271 | randomize(rand); 272 | } 273 | 274 | const nonce = i2h(rand[0]) + i2h(rand[1]); 275 | const tmpSessionID = i2h(rand[2]) + i2h(rand[3]); 276 | 277 | const bindMsg = new MessageV1( 278 | build({ 279 | _: 'bind_auth_key_inner', 280 | nonce, 281 | temp_auth_key_id: tempKey.id, 282 | perm_auth_key_id: permKey.id, 283 | temp_session_id: tmpSessionID, 284 | expires_at: tempKey.expires, 285 | }), 286 | true, 287 | ); 288 | 289 | bindMsg.salt = i2h(rand[4]) + i2h(rand[5]); 290 | bindMsg.sessionID = i2h(rand[6]) + i2h(rand[7]); 291 | bindMsg.id = msgID; 292 | bindMsg.buf[18] = rand[8]; 293 | bindMsg.buf[19] = rand[9]; 294 | 295 | return { 296 | perm_auth_key_id: permKey.id, 297 | nonce: i2h(rand[0]) + i2h(rand[1]), 298 | expires_at: tempKey.expires || 0, 299 | encrypted_message: i2ab(bindMsg.encrypt(permKey!.key, permKey!.id).buf), 300 | }; 301 | } 302 | 303 | /** 304 | * Binds temp auth key to permenent 305 | * Ref: https://core.telegram.org/method/auth.bindTempAuthKey 306 | */ 307 | export function bindTempAuthKey(client: ClientInterface, dc: number, permKey: AuthKey, tempKey: AuthKey, cb: (result: boolean) => void) { 308 | if (!tempKey || !permKey) { 309 | cb(false); 310 | return; 311 | } 312 | 313 | log(dc, 'binding temporary key'); 314 | 315 | const msgID = PlainMessage.GenerateID(); 316 | const rand = new Uint32Array(10); 317 | randomize(rand); 318 | 319 | client.dc.setSessionID(dc, i2h(rand[2]) + i2h(rand[3])); 320 | 321 | client.call('auth.bindTempAuthKey', createBindingEncryptedPayload(permKey, tempKey, msgID, rand), { msgID, dc, force: true }, (err, res) => { 322 | if (!err && res === true) { 323 | log(dc, 'temporary key successfuly binded'); 324 | client.dc.setKeyBinding(dc); 325 | if (cb) cb(true); 326 | } else { 327 | // cb(false); 328 | throw new Error('Auth: Binding temp auth key failed'); 329 | } 330 | }); 331 | } 332 | 333 | /** 334 | * Calls initConnection method invoked with layer 335 | */ 336 | export function initConnection(client: ClientInterface, dc: number, cb?: (result: boolean) => void) { 337 | const invokePrams = { 338 | layer: client.cfg.APILayer, 339 | query: { 340 | _: 'initConnection', 341 | api_id: client.cfg.APIID, 342 | device_model: client.cfg.deviceModel, 343 | system_version: client.cfg.systemVersion, 344 | app_version: client.cfg.appVersion, 345 | system_lang_code: client.cfg.langCode, 346 | lang_pack: '', 347 | lang_code: client.cfg.langCode, 348 | query: { _: 'help.getNearestDc' }, 349 | }, 350 | }; 351 | 352 | client.call('invokeWithLayer', invokePrams, { dc, force: true }, (err, res) => { 353 | if (err || !res) { 354 | log('Unexpected initConnection response'); 355 | if (cb) cb(false); 356 | } else { 357 | client.dc.setLayer(dc, client.cfg.APILayer); 358 | log('session successfuly inited'); 359 | if (cb) cb(true); 360 | } 361 | }); 362 | } 363 | 364 | /** 365 | * Calls auth.exportAuthorization and auth.importAuthorization from one dc to another 366 | */ 367 | export function transferAuthorization(client: ClientInterface, userID: number, dcFrom: number, dcTo: number, cb?: (res: boolean) => void) { 368 | client.call('auth.exportAuthorization', { dc_id: dcTo }, { dc: dcFrom, force: true }, (err, res) => { 369 | if (err || !res || res._ !== 'auth.exportedAuthorization') { 370 | if (cb) cb(false); 371 | return; 372 | } 373 | 374 | const { bytes } = res; 375 | 376 | client.call('auth.importAuthorization', { id: userID, bytes }, { dc: dcTo, force: true }, (err2, res2) => { 377 | if (err2 || !res2 || res2._ !== 'auth.authorization') { 378 | if (cb) cb(false); 379 | return; 380 | } 381 | 382 | if (cb) cb(true); 383 | }); 384 | }); 385 | } 386 | -------------------------------------------------------------------------------- /src/client/client.test.ts: -------------------------------------------------------------------------------- 1 | import Client from './client'; 2 | import configMock from '../mock/transport_config'; 3 | import { Socket, Http } from '../transport'; 4 | import { randomize } from '../serialization'; 5 | import { ResPQ } from '../tl/mtproto/types'; 6 | import metaMock from '../mock/client_meta'; 7 | import { AuthKey } from './types'; 8 | 9 | test('client | common', () => { 10 | const client = new Client({ 11 | ...configMock, 12 | meta: metaMock, 13 | autoConnect: false, 14 | transport: 'websocket', 15 | }); 16 | 17 | expect(client.instances[0] instanceof Socket).toBeTruthy(); 18 | 19 | const nonce = new Uint32Array(4); 20 | randomize(nonce); 21 | 22 | let connected = false; 23 | let metaChanged = false; 24 | client.on('networkChanged', (state) => { connected = state === 'connected'; }); 25 | client.on('metaChanged', () => { metaChanged = true; }); 26 | client.dc.setSalt(1, '0102030405060708'); 27 | 28 | const async = new Promise((resolve) => { 29 | client.plainCall('req_pq', { nonce }, (err, result) => { 30 | expect(err).toBe(null); 31 | expect(connected).toBeTruthy(); 32 | expect(metaChanged).toBeTruthy(); 33 | resolve(result); 34 | }); 35 | }); 36 | 37 | return async.then((result) => { 38 | expect(result._).toEqual('resPQ'); 39 | expect(result.nonce).toEqual(nonce); 40 | }); 41 | }); 42 | 43 | test('client | http', () => { 44 | const client = new Client({ 45 | ...configMock, 46 | meta: metaMock, 47 | autoConnect: false, 48 | transport: 'http', 49 | }); 50 | 51 | expect(client.instances[0] instanceof Http).toBeTruthy(); 52 | }); 53 | 54 | test('client | authorization no-pfs', () => { 55 | const client = new Client({ 56 | ...configMock, 57 | meta: { 58 | ...metaMock, 59 | pfs: false, 60 | }, 61 | autoConnect: false, 62 | }); 63 | 64 | const async = new Promise((resolve) => { 65 | client.authorize(1, resolve); 66 | }); 67 | 68 | return async.then((key) => { 69 | if (!key) throw new Error('key should be null'); 70 | expect(key.id.length).toEqual(16); 71 | expect(key.key.length).toEqual(64); 72 | expect(!key.expires).toEqual(true); 73 | expect(client.dc.pfs()).toEqual(false); 74 | }); 75 | }, 150000); 76 | 77 | test('client | authorization pfs', () => { 78 | const client = new Client({ 79 | ...configMock, 80 | meta: metaMock, 81 | autoConnect: false, 82 | }); 83 | 84 | const async = new Promise((resolve) => { 85 | client.authorize(1, resolve); 86 | }); 87 | 88 | return async.then((key) => { 89 | if (!key) throw new Error('key should be null'); 90 | expect(key.id.length).toEqual(16); 91 | expect(key.key.length).toEqual(64); 92 | }); 93 | }, 150000); 94 | -------------------------------------------------------------------------------- /src/client/dc.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import DCService from './dc'; 3 | import metaMock from '../mock/client_meta'; 4 | import { permanentKey, temporaryKey } from '../mock/auth_key'; 5 | import { ClientMeta } from './types'; 6 | 7 | test('dc service | common', () => { 8 | let changed = 0; 9 | 10 | const onMetaChange = (_meta: ClientMeta) => { 11 | changed += 1; 12 | }; 13 | 14 | const service = new DCService(metaMock, onMetaChange); 15 | const salt = '1234567812345678'; 16 | const session = '0102030401020304'; 17 | const userID = 123456; 18 | 19 | expect(service.getHost(2)).toBe('venus-1.web.telegram.org'); 20 | expect(service.getUserID()).toBe(null); 21 | 22 | service.setPermanentKey(2, permanentKey); 23 | service.setTemporaryKey(2, temporaryKey); 24 | service.setSalt(2, salt); 25 | service.setKeyBinding(2); 26 | service.setAuthorization(2, userID); 27 | service.setConnection(2); 28 | 29 | expect(service.meta).toEqual({ 30 | ...metaMock, 31 | userID, 32 | dcs: { 33 | 2: { 34 | temporaryKey: { 35 | ...temporaryKey, 36 | binded: true, 37 | }, 38 | permanentKey, 39 | salt, 40 | authorized: true, 41 | inited: true, 42 | }, 43 | }, 44 | }); 45 | 46 | expect(service.getUserID()).toBe(userID); 47 | expect(changed).toEqual(6); 48 | 49 | service.setBaseDC(3); 50 | expect(service.meta.baseDC).toEqual(3); 51 | 52 | expect(changed).toEqual(7); 53 | 54 | service.setSessionID(2, session); 55 | 56 | expect(service.sessions).toEqual({ 2: session }); 57 | expect(service.getSessionID(2)).toEqual(session); 58 | 59 | expect(service.getAuthKey(2)).toBe(temporaryKey); 60 | expect(service.getKeyBinding(2)).toBe(true); 61 | expect(service.getConnection(2)).toBe(true); 62 | 63 | service.setTemporaryKey(2, null); 64 | expect(service.getAuthKey(2)).toBe(null); 65 | 66 | expect(service.getAuthKey(3)).toBe(null); 67 | expect(service.getPermanentKey(3)).toBe(null); 68 | 69 | service.meta.pfs = false; 70 | 71 | expect(service.getAuthKey(2)).toBe(permanentKey); 72 | 73 | expect(service.nextSeqNo(3, true)).toBe(3); 74 | expect(service.nextSeqNo(3)).toBe(4); 75 | expect(service.nextSeqNo(3, true)).toBe(5); 76 | 77 | expect(service.getKeyBinding(2)).toBe(false); 78 | expect(service.getAuthorization(2)).toBe(true); 79 | expect(service.getConnection(2)).toBe(false); 80 | expect(service.getSalt(2)).toBe(salt); 81 | 82 | expect(service.pfs()).toBe(false); 83 | 84 | expect(service.getSalt(3)).not.toBe(''); 85 | expect(service.getSessionID(3)).not.toBe(''); 86 | 87 | 88 | expect(service.getPermanentKey(2)).toEqual(permanentKey); 89 | 90 | expect(service.getKeyBinding(3)).toEqual(false); 91 | expect(service.getAuthKey(3)).toBe(null); 92 | }); 93 | -------------------------------------------------------------------------------- /src/client/dc.ts: -------------------------------------------------------------------------------- 1 | import { randomize, i2h } from '../serialization'; 2 | import { ClientMeta, AuthKey, ClientMetaData, AuthKeyNotNull } from './types'; 3 | 4 | type DCConfig = { 5 | host: string, 6 | media: boolean, 7 | }; 8 | 9 | type MetaUpdateCallback = (data: ClientMeta) => void; 10 | 11 | /** 12 | * Helper class for managing datacenters 13 | */ 14 | export default class DCService { 15 | static Config: Record = { 16 | 1: { host: 'pluto', media: true }, 17 | 2: { host: 'venus', media: true }, 18 | 3: { host: 'aurora', media: true }, 19 | 4: { host: 'vesta', media: true }, 20 | 5: { host: 'flora', media: true }, 21 | }; 22 | 23 | /** On change callback */ 24 | callback?: MetaUpdateCallback; 25 | 26 | /** Datacenter meta data */ 27 | meta: ClientMetaData; 28 | 29 | sessions: Record = {}; 30 | seqNos: Record = {}; 31 | 32 | constructor(initial: ClientMetaData, callback?: MetaUpdateCallback) { 33 | this.meta = initial; 34 | this.callback = callback; 35 | this.sessions = {}; 36 | this.seqNos = {}; 37 | } 38 | 39 | /** Resolve hostname by dc id */ 40 | getHost(dc: number) { 41 | return `${DCService.Config[dc].host}${DCService.Config[dc].media ? '-1' : ''}.web.telegram.org`; 42 | } 43 | 44 | triggerUpdateEvent() { 45 | if (this.callback) this.callback(this.meta); 46 | } 47 | 48 | getDC(dcID: number) { 49 | if (!this.meta.dcs[dcID]) this.meta.dcs[dcID] = {}; 50 | return this.meta.dcs[dcID]; 51 | } 52 | 53 | setBaseDC(dcID: number) { 54 | this.meta.baseDC = dcID; 55 | this.triggerUpdateEvent(); 56 | } 57 | 58 | setSalt(dcID: number, salt: string) { 59 | this.getDC(dcID).salt = salt; 60 | this.triggerUpdateEvent(); 61 | } 62 | 63 | setAuthorization(dcID: number, userID: number) { 64 | this.meta.userID = userID; 65 | this.meta.authorized = true; 66 | this.getDC(dcID).authorized = true; 67 | this.triggerUpdateEvent(); 68 | } 69 | 70 | setConnection(dcID: number) { 71 | this.getDC(dcID).inited = true; 72 | this.triggerUpdateEvent(); 73 | } 74 | 75 | setLayer(dcID: number, layer: number) { 76 | this.getDC(dcID).layer = layer; 77 | this.triggerUpdateEvent(); 78 | } 79 | 80 | setPermanentKey(dcID: number, key: AuthKeyNotNull) { 81 | this.getDC(dcID).permanentKey = key; 82 | this.triggerUpdateEvent(); 83 | } 84 | 85 | setTemporaryKey(dcID: number, key: AuthKey) { 86 | this.getDC(dcID).temporaryKey = key; 87 | if (key === null) this.getDC(dcID).inited = false; 88 | this.triggerUpdateEvent(); 89 | } 90 | 91 | setKeyBinding(dcID: number) { 92 | this.getDC(dcID).temporaryKey!.binded = true; 93 | this.triggerUpdateEvent(); 94 | } 95 | 96 | setSessionID(dcID: number, session: string) { 97 | this.sessions[dcID] = session; 98 | this.seqNos[dcID] = 1; 99 | } 100 | 101 | getKeyBinding(dcID: number): boolean { 102 | if (!this.getDC(dcID).temporaryKey) return false; 103 | return this.getDC(dcID).temporaryKey!.binded || false; 104 | } 105 | 106 | getSessionID(dcID: number): string { 107 | if (!this.sessions[dcID]) { 108 | const rand = new Uint32Array(2); 109 | randomize(rand); 110 | this.sessions[dcID] = i2h(rand[0]) + i2h(rand[1]); 111 | } 112 | 113 | return this.sessions[dcID]; 114 | } 115 | 116 | getSalt(dcID: number): string { 117 | const dc = this.getDC(dcID); 118 | if (!dc.salt) { 119 | const rand = new Uint32Array(2); 120 | randomize(rand); 121 | dc.salt = i2h(rand[0]) + i2h(rand[1]); 122 | } 123 | 124 | return dc.salt!; 125 | } 126 | 127 | getAuthKey(dcID: number): AuthKey { 128 | if (this.meta.pfs) return this.getDC(dcID).temporaryKey || null; 129 | return this.getDC(dcID).permanentKey || null; 130 | } 131 | 132 | getPermanentKey(dcID: number) { 133 | return this.getDC(dcID).permanentKey || null; 134 | } 135 | 136 | getUserID(): number | null { 137 | return this.meta.userID || null; 138 | } 139 | 140 | getAuthorization(dcID: number): boolean { 141 | return !!this.getDC(dcID).authorized; 142 | } 143 | 144 | getConnection(dcID: number): boolean { 145 | return !!this.getDC(dcID).inited; 146 | } 147 | 148 | getLayer(dcID: number): number | undefined { 149 | return this.getDC(dcID).layer; 150 | } 151 | 152 | pfs(): boolean { 153 | return this.meta.pfs; 154 | } 155 | 156 | /** 157 | * Increment msg_seq_no if message is content related 158 | * Ref: https://core.telegram.org/mtproto/description#message-sequence-number-msg-seqno 159 | */ 160 | nextSeqNo(dcID: number, isContentRelated: boolean = false): number { 161 | if (!this.seqNos[dcID]) this.seqNos[dcID] = 1; 162 | 163 | const isc = isContentRelated ? 1 : 0; 164 | const seqNo = this.seqNos[dcID]; 165 | 166 | this.seqNos[dcID] += isc; 167 | 168 | return seqNo * 2 + isc; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Client } from './client'; 2 | export { default as DCService } from './dc'; 3 | -------------------------------------------------------------------------------- /src/client/rpc.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import client from '../mock/client'; 3 | import RPCService from './rpc'; 4 | import { Message } from '../message'; 5 | import { build } from '../tl'; 6 | 7 | test('rpc service | common', () => { 8 | const service = new RPCService(client); 9 | let raised = 0; 10 | 11 | const id = '51e57ac42770964a'; 12 | const dc = 1; 13 | const thread = 1; 14 | const transport = 'websocket'; 15 | const msg = new Message(build({ _: 'help.getNearestDc' }), true); 16 | msg.id = id; 17 | 18 | // listen server answer 19 | service.subscribe(msg, { dc, thread, transport }, (err, result) => { 20 | raised++; 21 | 22 | if (err || !result) throw new Error('Expected result'); 23 | expect(result._).toBe('nearestDc'); 24 | }); 25 | 26 | // emulate response 27 | const result = { 28 | _: 'rpc_result', 29 | req_msg_id: id, 30 | result: { 31 | _: 'nearestDc', 32 | country: 'ru', 33 | this_dc: 1, 34 | nearest_dc: 1, 35 | }, 36 | }; 37 | 38 | service.processMessage(result, { 39 | dc: 1, 40 | thread: 1, 41 | id: '51E57AC91E83C801', 42 | transport: 'websocket', 43 | }); 44 | 45 | expect(raised).toBe(1); 46 | expect(service.requests).toStrictEqual({}); 47 | }); 48 | -------------------------------------------------------------------------------- /src/client/rpc.ts: -------------------------------------------------------------------------------- 1 | import { logs } from '../utils/log'; 2 | import { Message } from '../message'; 3 | import { ab2i, Reader32 } from '../serialization'; 4 | import { RPCHeaders, ClientError, ClientInterface, ClientConfig, RequestRPC, PlainCallback, MessageHeaders } from './types'; 5 | import { parse } from '../tl'; 6 | import { Object, BadMsgNotification, NewSession, RpcResult } from '../tl/mtproto/types'; 7 | 8 | const { inflate } = require('pako/lib/inflate'); // fix for rollup 9 | 10 | const debug = (cfg: ClientConfig, ...rest: any[]) => { 11 | if (cfg.debug) logs('rpc')(...rest); 12 | }; 13 | 14 | /** 15 | * Service class helper for processing rpc messages 16 | */ 17 | export default class RPCService { 18 | /** Client Handler */ 19 | client: ClientInterface; 20 | 21 | /** Outcoming requests */ 22 | requests: Record; 23 | 24 | /** Pending message acknowlegments */ 25 | pendingAcks: Record; 26 | 27 | /** 28 | * Creates auth service object 29 | */ 30 | constructor(client: ClientInterface) { 31 | this.client = client; 32 | 33 | this.requests = {}; 34 | this.pendingAcks = []; 35 | } 36 | 37 | /** 38 | * Subscribes callback to message identificator 39 | */ 40 | subscribe(message: Message, headers: MessageHeaders, cb?: PlainCallback) { 41 | if (this.requests[message.id] && !cb) { 42 | cb = this.requests[message.id].cb; 43 | } 44 | 45 | this.requests[message.id] = { 46 | message, 47 | headers, 48 | cb, 49 | }; 50 | 51 | if (cb) debug(this.client.cfg, headers.dc, '<- request', message.id, `(thread: ${headers.thread}, seq: ${message.seqNo})`); 52 | } 53 | 54 | /** 55 | * Call callback due to message id 56 | */ 57 | emit(id: string, error: ClientError, data?: any) { 58 | if (!id) return; 59 | 60 | if (!this.requests[id]) { 61 | debug(this.client.cfg, 'unknown request for ', id); 62 | return; 63 | } 64 | 65 | const request = this.requests[id]; 66 | 67 | // Call callback with error 68 | if (error !== null) { 69 | if (request.cb) request.cb(error); 70 | delete this.requests[id]; 71 | return; 72 | } 73 | 74 | // Success response 75 | if (data === undefined) { 76 | throw new Error('Expected type language constructor for response'); 77 | } 78 | 79 | if (data && data._updates) { 80 | this.client.updates.process(data); 81 | } 82 | 83 | // Process response 84 | debug(this.client.cfg, request.headers.dc, 'rpc result', request.headers.method, '-> ', data, `(request id: ${id})`); 85 | 86 | if (request.cb) request.cb(null, data); 87 | 88 | delete this.requests[id]; 89 | 90 | // Apply middleware 91 | this.middleware(request, data); 92 | } 93 | 94 | /** 95 | * Middlewares 96 | */ 97 | middleware = (request: RequestRPC, result: any) => { 98 | if (result._ === 'auth.authorization') { 99 | debug(this.client.cfg, 'middleware', result._); 100 | this.client.dc.setAuthorization(request.headers.dc!, result.user.id); 101 | this.client.authorize(2); 102 | } 103 | }; 104 | 105 | /** 106 | * Resends request message by id 107 | */ 108 | resend(id: string, forceChangeSeq: boolean = false) { 109 | const request = this.requests[id]; 110 | 111 | if (!request) { 112 | console.warn(`Cannot resend missing request ${id}`); // eslint-disable-line no-console 113 | return; 114 | } 115 | 116 | request.message.salt = this.client.dc.getSalt(request.headers.dc); 117 | if (forceChangeSeq) request.message.seqNo = this.client.dc.nextSeqNo(request.headers.dc, true); 118 | 119 | this.client.send(request.message, request.headers); 120 | 121 | debug(this.client.cfg, request.headers.dc, '<- re-sent', id); 122 | } 123 | 124 | /** 125 | * Adds message ID to ack pending list 126 | */ 127 | ackMsg(transport: string, dc: number, thread: number, ...ids: string[]) { 128 | const key = thread * 10 + dc + (transport === 'websocket' ? 1000 : 0); 129 | 130 | if (!this.pendingAcks[key]) this.pendingAcks[key] = []; 131 | 132 | for (let i = 0; i < ids.length; i += 1) { 133 | if (this.pendingAcks[key].indexOf(ids[i]) === -1) this.pendingAcks[key].push(ids[i]); 134 | } 135 | } 136 | 137 | /** 138 | * Sends message acks from pending list 139 | */ 140 | sendAcks(transport: string, dc: number, thread: number) { 141 | const key = thread * 10 + dc + (transport === 'websocket' ? 1000 : 0); 142 | 143 | if (!this.pendingAcks[key]) this.pendingAcks[key] = []; 144 | 145 | if (this.pendingAcks[key].length > 0) { 146 | const ids = this.pendingAcks[key].map((id) => id); 147 | 148 | this.client.call('msgs_ack', { msg_ids: ids }, { dc, thread, force: true }); 149 | this.pendingAcks[key] = []; 150 | } 151 | } 152 | 153 | /** 154 | * Processes RPC response messages 155 | */ 156 | processMessage(result: any, headers: RPCHeaders, ack: boolean = true) { 157 | debug(this.client.cfg, headers.dc, '->', result._); 158 | 159 | switch (result._) { 160 | case 'msg_container': this.processMessageContainer(result, headers); break; 161 | case 'new_session_created': this.processSessionCreated(result, headers); break; 162 | case 'bad_server_salt': this.processBadServerSalt(result, headers); break; 163 | case 'bad_msg_notification': this.processBadMsgNotification(result, headers); break; 164 | case 'msgs_ack': break; 165 | case 'gzip_packed': this.processGzipped(result, headers); break; 166 | case 'rpc_result': this.processRPCResult(result, headers); break; 167 | case 'msg_detailed_info': break; 168 | 169 | default: 170 | // send acknowlegment 171 | if (headers.id) this.ackMsg(headers.transport, headers.dc, headers.thread, headers.id); 172 | 173 | // updates 174 | if (result._update) { 175 | this.client.updates.process(result); 176 | return; 177 | } 178 | 179 | console.warn('unknown', result._, result); // eslint-disable-line no-console 180 | debug(this.client.cfg, headers.dc, '-> unknown %s', result._, result); 181 | 182 | break; 183 | } 184 | 185 | if (ack) this.sendAcks(headers.transport, headers.dc, headers.thread); 186 | } 187 | 188 | /** 189 | * Process: gzip_packed 190 | */ 191 | processGzipped(result: Object.gzip_packed, headers: RPCHeaders) { 192 | try { 193 | const gz = new Uint8Array(result.packed_data); 194 | const reader = new Reader32(ab2i(inflate(gz).buffer)); 195 | this.processMessage(parse(reader), headers, false); 196 | } catch (e) { 197 | console.warn('Unable to decode gzip data', e); // eslint-disable-line no-console 198 | } 199 | } 200 | 201 | /** 202 | * Process: msg_container 203 | */ 204 | processMessageContainer(result: any /* MessageContainer.msg_container */, headers: RPCHeaders) { 205 | for (let i = 0; i < result.messages.length; i += 1) { 206 | const item = result.messages[i]; 207 | 208 | this.ackMsg(headers.transport, headers.dc, headers.thread, item.msg_id); 209 | 210 | this.processMessage(item.body, { 211 | ...headers, 212 | id: item.msg_id, 213 | }, false); 214 | } 215 | } 216 | 217 | /** 218 | * Process: bad_server_salt 219 | */ 220 | processBadServerSalt(result: BadMsgNotification.bad_server_salt, headers: RPCHeaders) { 221 | debug(this.client.cfg, headers.dc, '-> bad_server_salt', `(${headers.transport}, thread: ${headers.thread})`); 222 | 223 | if (headers.id) this.ackMsg(headers.transport, headers.dc, headers.thread, headers.id); 224 | 225 | this.client.dc.setSalt(headers.dc, result.new_server_salt); 226 | this.resend(result.bad_msg_id); 227 | } 228 | 229 | /** 230 | * Processes: new_session_created 231 | */ 232 | processSessionCreated(result: NewSession.new_session_created, headers: RPCHeaders) { 233 | debug(this.client.cfg, headers.dc, '-> new_session_created', `(${headers.transport}, thread: ${headers.thread})`); 234 | 235 | if (headers.id) this.ackMsg(headers.transport, headers.dc, headers.thread, headers.id); 236 | 237 | this.client.dc.setSalt(headers.dc, result.server_salt); 238 | } 239 | 240 | /** 241 | * Processes: bad_msg_notification 242 | */ 243 | processBadMsgNotification(result: BadMsgNotification.bad_msg_notification, headers: RPCHeaders) { 244 | debug(this.client.cfg, headers.dc, '-> bad_msg_notification', result.bad_msg_id, result.error_code, 'sec:', result.bad_msg_seqno); 245 | 246 | if (result.error_code === 32) { 247 | this.resend(result.bad_msg_id, true); 248 | } 249 | 250 | // To Do: sync server time 251 | 252 | if (headers.id) this.ackMsg(headers.transport, headers.dc, headers.thread, headers.id); 253 | } 254 | 255 | /** 256 | * Processes: rpc_result 257 | */ 258 | processRPCResult(res: RpcResult.rpc_result, headers: RPCHeaders) { 259 | let { result } = res as any; 260 | const reqID = res.req_msg_id; 261 | 262 | if (headers.id) this.ackMsg(headers.transport, headers.dc, headers.thread, headers.id); 263 | 264 | // Ungzip if gzipped 265 | if (result && result._ === 'gzip_packed') { 266 | const gz = new Uint8Array(result.packed_data); 267 | const reader = new Reader32(ab2i(inflate(gz))); 268 | result = parse(reader) as any; 269 | } 270 | 271 | switch (result._) { 272 | case 'rpc_error': 273 | this.emit(reqID, { 274 | type: 'rpc', 275 | code: result.error_code, 276 | message: result.error_message, 277 | }, result); 278 | 279 | debug(this.client.cfg, headers.dc, '-> rpc_error', reqID, `(${headers.transport}, thread: ${headers.thread})`, result); 280 | break; 281 | 282 | default: 283 | this.emit(reqID, null, result); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/client/types.ts: -------------------------------------------------------------------------------- 1 | import { MTProtoTransport } from '../transport/protocol'; 2 | import { MethodDeclMap } from '../tl'; 3 | import { Message } from '../message'; 4 | 5 | export type Transports = 'http' | 'websocket'; 6 | 7 | /** Client configuration type */ 8 | export type ClientConfig = { 9 | test: boolean, 10 | debug: boolean, 11 | ssl: boolean, 12 | dc: number, 13 | protocol: MTProtoTransport, 14 | transport: Transports, 15 | meta: Record, 16 | 17 | APILayer: number, 18 | APIID?: string, 19 | APIHash?: string, 20 | 21 | deviceModel: string, 22 | systemVersion: string, 23 | appVersion: string, 24 | langCode: string, 25 | 26 | autoConnect: boolean, 27 | }; 28 | 29 | /** Default client configuration */ 30 | export const defaultClientConfig: ClientConfig = { 31 | test: false, 32 | debug: false, 33 | ssl: true, 34 | dc: 2, 35 | protocol: 'intermediate' as MTProtoTransport, 36 | transport: 'websocket' as Transports, 37 | meta: {}, 38 | 39 | APILayer: 113, 40 | deviceModel: 'Unknown', 41 | systemVersion: 'Unknown', 42 | appVersion: '1.0.0', 43 | langCode: 'en', 44 | 45 | autoConnect: true, 46 | }; 47 | 48 | export type RequestRPC = { 49 | message: Message, 50 | headers: MessageHeaders, 51 | cb?: PlainCallback, 52 | }; 53 | 54 | /** Generic error for mtproto client */ 55 | export type ClientError = { 56 | type: 'rpc' | 'network' | 'transport' | 'internal'; 57 | code: number, 58 | message?: string, 59 | } | null; 60 | 61 | /** Request callback */ 62 | export type RequestCallback = (error: ClientError | null, result?: undefined | any) => void; 63 | 64 | /** Request callback */ 65 | export type PlainCallback = (error: ClientError | null, result?: MethodDeclMap[K]['res']) => void; 66 | 67 | /** DCService interface to avoid dependency cycle */ 68 | export interface DCServiceInterface { 69 | getHost(dc: number): string; 70 | setSalt(dcID: number, salt: string): void; 71 | setAuthorization(dcID: number, userID: number): void; 72 | setConnection(dcID: number): void; 73 | setPermanentKey(dcID: number, key: AuthKeyNotNull): void; 74 | setTemporaryKey(dcID: number, key: AuthKey): void; 75 | setKeyBinding(dcID: number): void; 76 | setSessionID(dcID: number, session: string): void; 77 | setLayer(dcID: number, layer: number): void; 78 | getKeyBinding(dcID: number): boolean; 79 | getSessionID(dcID: number): string; 80 | getSalt(dcID: number): string; 81 | getAuthKey(dcID: number): AuthKey; 82 | getUserID(): number | null; 83 | getAuthorization(dcID: number): boolean; 84 | getConnection(dcID: number): boolean; 85 | pfs(): boolean; 86 | nextSeqNo(dcID: number, isContentRelated?: boolean): number; 87 | } 88 | 89 | export interface UpdateServiceInterface { 90 | process(updateMsg: any): void; 91 | } 92 | 93 | /** Client interface to avoid dependency cycle */ 94 | export interface ClientInterface { 95 | cfg: ClientConfig; 96 | dc: DCServiceInterface; 97 | updates: UpdateServiceInterface; 98 | authorize(dc: number, cb?: (key: AuthKey) => void): void; 99 | plainCall(method: K, data: MethodDeclMap[K]['req'], cb?: PlainCallback): void; 100 | plainCall(method: K, data: MethodDeclMap[K]['req'], headers: CallHeaders, cb?: PlainCallback): void; 101 | call(method: K, data: MethodDeclMap[K]['req'], cb?: PlainCallback): void; 102 | call(method: K, data: MethodDeclMap[K]['req'], headers: CallHeaders, cb?: PlainCallback): void; 103 | call(method: 'msgs_ack', data: { msg_ids: string[] }, headers: CallHeaders): void; 104 | send(msg: Message, headers: CallHeaders, cb?: PlainCallback): void; 105 | } 106 | 107 | /** Authorization key info with PFS */ 108 | export type AuthKeyNotNull = { 109 | key: Uint32Array, 110 | id: string, 111 | expires?: number, 112 | binded?: boolean, 113 | }; 114 | 115 | export type AuthKey = null | AuthKeyNotNull; 116 | 117 | export type DataCenterMetaData = { 118 | permanentKey?: AuthKey, 119 | temporaryKey?: AuthKey, 120 | salt?: string, 121 | inited?: boolean, 122 | layer?: number, 123 | authorized?: boolean, 124 | }; 125 | 126 | export type ClientMetaData = { 127 | pfs: boolean, 128 | baseDC: number, 129 | userID?: number, 130 | authorized?: boolean, 131 | dcs: Record, 132 | }; 133 | 134 | /** Datacenter info */ 135 | export type DatacenterMeta = { 136 | permKey?: AuthKey, 137 | tempKey?: AuthKey, 138 | salt?: string, 139 | sessionID?: string, 140 | sessionExpire?: number, 141 | connectionInited?: boolean, 142 | seqNo?: number, 143 | userID?: number, 144 | [key: string]: any, 145 | }; 146 | 147 | /** Client Datacenter Meta info */ 148 | export type ClientMeta = Record; 149 | 150 | export type CallHeaders = { 151 | dc?: number, 152 | thread?: number, 153 | transport?: Transports, 154 | msgID?: string, 155 | force?: boolean, 156 | method?: string, 157 | }; 158 | 159 | export type MessageHeaders = CallHeaders & { 160 | dc: number, 161 | thread: number, 162 | transport: string, 163 | }; 164 | 165 | /** Headers for recursive processing rpc messages */ 166 | export type RPCHeaders = { 167 | id: string, 168 | dc: number, 169 | thread: number, 170 | transport: string, 171 | }; 172 | -------------------------------------------------------------------------------- /src/client/updates.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import UpdateService from './updates'; 3 | 4 | test('update service | events', () => { 5 | const service = new UpdateService(); 6 | const updateMsg = { _: 'updateShortMessage' }; 7 | 8 | let raised = 0; 9 | 10 | service.on('updateShortMessage', (update: any) => { 11 | expect(update._).toBe('updateShortMessage'); 12 | raised++; 13 | }); 14 | 15 | service.on((update: any) => { 16 | expect(update._).toBe('updateShortMessage'); 17 | raised++; 18 | }); 19 | 20 | service.process(updateMsg); 21 | 22 | expect(raised).toBe(2); 23 | }); 24 | -------------------------------------------------------------------------------- /src/client/updates.ts: -------------------------------------------------------------------------------- 1 | import { logs } from '../utils/log'; 2 | import { ClientInterface, PlainCallback } from './types'; 3 | import { UpdateDeclMap } from '../tl'; 4 | 5 | // debug helper 6 | const debug = (client?: ClientInterface, ...rest: any[]) => { 7 | if (client && client.cfg.debug) logs('updates')(...rest); 8 | }; 9 | 10 | export type UpdateListener = (update: UpdateDeclMap[K]) => void; 11 | 12 | /** 13 | * Service class for handling update messages 14 | */ 15 | export default class UpdateService { 16 | /** Type Language Handler */ 17 | client?: ClientInterface; 18 | 19 | /** Subscribers */ 20 | subscribers: Record[]>; 21 | 22 | /** Subscriber on any update */ 23 | subscriberAny?: UpdateListener; 24 | 25 | /** 26 | * Creates auth service object 27 | */ 28 | constructor(client?: ClientInterface) { 29 | this.client = client; 30 | this.subscribers = {}; 31 | } 32 | 33 | /** Fetches update state */ 34 | fetch(cb?: PlainCallback<'updates.getState'>) { 35 | if (!this.client) throw new Error('Unable to fetch updates without client instance'); 36 | this.client.call('updates.getState', {}, cb); 37 | } 38 | 39 | /** 40 | * Calls specific callback on update 41 | */ 42 | emit(update: any) { 43 | const listeners = this.subscribers[update._]; 44 | if (listeners) { 45 | for (let i = 0; i < listeners.length; i += 1) listeners[i](update); 46 | } 47 | 48 | debug(this.client, update._); 49 | } 50 | 51 | /** 52 | * Calls special update events like mentioned users, chats, vectors 53 | */ 54 | emitSpecial(predicate: string, data: any) { 55 | const listeners = this.subscribers[predicate]; 56 | if (listeners) { 57 | for (let i = 0; i < listeners.length; i += 1) listeners[i](data); 58 | } 59 | } 60 | 61 | /** 62 | * Subscribes specific callback on update 63 | */ 64 | on(predicate: K, reciever: UpdateListener): void; 65 | on(reciever: UpdateListener): void; 66 | on(arg0: string | UpdateListener, arg1?: UpdateListener): void { 67 | if (typeof arg0 === 'string') { 68 | if (!this.subscribers[arg0]) this.subscribers[arg0] = []; 69 | if (arg1) this.subscribers[arg0].push(arg1); 70 | } else { 71 | this.subscriberAny = arg0; 72 | } 73 | } 74 | 75 | /** 76 | * Processes update messages 77 | * Ref: https://core.telegram.org/api/updates 78 | */ 79 | process(updateMsg: any) { 80 | if (this.subscriberAny) this.subscriberAny(updateMsg); 81 | 82 | switch (updateMsg._) { 83 | // Ref: https://core.telegram.org/constructor/updateShort 84 | case 'updateShort': 85 | this.emit(updateMsg.update); 86 | break; 87 | 88 | // Ref: https://core.telegram.org/type/Updates 89 | case 'updateShortMessage': 90 | case 'updateShortSentMessage': 91 | case 'updateShortChatMessage': 92 | this.emit(updateMsg); 93 | break; 94 | 95 | // Ref: https://core.telegram.org/constructor/updates 96 | case 'updatesCombined': 97 | case 'updates': 98 | // process users 99 | if (updateMsg.users) { 100 | for (let i = 0; i < updateMsg.users.length; i += 1) { 101 | this.emitSpecial('user', updateMsg.users); 102 | } 103 | } 104 | 105 | // process chats 106 | if (updateMsg.chats) { 107 | for (let i = 0; i < updateMsg.chats.length; i += 1) { 108 | this.emitSpecial('chat', updateMsg.chats[i]); 109 | } 110 | } 111 | 112 | // process updates 113 | if (updateMsg.updates) { 114 | for (let i = 0; i < updateMsg.updates.length; i += 1) { 115 | this.emit(updateMsg.updates[i]); 116 | } 117 | } 118 | break; 119 | 120 | // todo: handle updatesTooLong 121 | // Ref: https://core.telegram.org/api/updates#recovering-gaps 122 | 123 | default: 124 | debug(this.client, 'unknown', updateMsg._, updateMsg); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/crypto/pq.test.ts: -------------------------------------------------------------------------------- 1 | import BigInt from 'big-integer'; 2 | import { pqPrimePollard, BrentPrime } from './pq'; 3 | 4 | test('calc | pq', () => { 5 | const pq = BigInt('17ED48941A08F981', 16); 6 | 7 | expect(pqPrimePollard(pq)).toEqual([BigInt('494C553B', 16), BigInt('53911073', 16)]); 8 | expect(BrentPrime(pq)).toEqual([BigInt('494C553B', 16), BigInt('53911073', 16)]); 9 | }); 10 | -------------------------------------------------------------------------------- /src/crypto/pq.ts: -------------------------------------------------------------------------------- 1 | type BigInteger = import('big-integer').BigInteger; 2 | const BigInt = require('big-integer'); // fix for rollup 3 | 4 | /** 5 | * Prime factorization p-Pollard Algorithm. 6 | * o(n^1/4) 7 | */ 8 | export function pqPrimePollard(pq: BigInteger): BigInteger[] { 9 | const n = pq; 10 | 11 | const F = (x: BigInteger): BigInteger => x.multiply(x).subtract(BigInt.one); 12 | 13 | let x = BigInt.randBetween(1, n); 14 | let y = BigInt.one; 15 | let stage = 2; 16 | let i = 0; 17 | 18 | let gcd = BigInt.gcd(n, (x.subtract(y)).abs()); 19 | 20 | while (gcd.equals(BigInt.one)) { 21 | if (i === stage) { 22 | y = x; 23 | stage *= 2; 24 | } 25 | 26 | x = F(x).mod(n); 27 | 28 | gcd = BigInt.gcd(n, (x.subtract(y)).abs()); 29 | 30 | i += 1; 31 | } 32 | 33 | const q = n.divide(gcd); 34 | 35 | return gcd.greater(q) ? [q, gcd] : [gcd, q]; 36 | } 37 | 38 | /** 39 | * Prime factorization Richard Brent's Algorithm 40 | */ 41 | export function BrentPrime(pq: BigInteger): BigInteger[] { 42 | const n = pq; 43 | 44 | let y = BigInt.randBetween(BigInt.one, n); 45 | const c = BigInt.randBetween(BigInt.one, n); 46 | const m = BigInt.randBetween(BigInt.one, n); 47 | 48 | let g = BigInt(1); let r = BigInt(1); let q = BigInt(1); let x = BigInt(0); let ys = BigInt(0); 49 | 50 | while (g.equals(BigInt.one)) { 51 | x = y; 52 | 53 | for (let i = 1; r.greaterOrEquals(BigInt(i)); i += 1) { 54 | y = y.multiply(y).mod(n).add(c).mod(n); 55 | } 56 | 57 | let k = BigInt(0); 58 | 59 | while (k.lesser(r) && g.equals(BigInt(1))) { 60 | ys = y; 61 | 62 | for (let i = 1; BigInt.min(m, r.subtract(k)).greaterOrEquals(BigInt(i)); i += 1) { 63 | y = y.multiply(y).mod(n).add(c).mod(n); 64 | q = q.multiply(x.subtract(y).abs()).mod(n); 65 | } 66 | 67 | g = BigInt.gcd(q, n); 68 | k = k.add(m); 69 | } 70 | 71 | r = r.multiply(BigInt(2)); 72 | } 73 | 74 | if (g.equals(n)) { 75 | // eslint-disable-next-line 76 | while (true) { 77 | ys = ys.multiply(ys).mod(n).add(c).mod(n); 78 | g = BigInt.gcd(x.subtract(ys).abs(), n); 79 | 80 | if (g.greater(BigInt.one)) break; 81 | } 82 | } 83 | 84 | const pout = n.divide(g); 85 | 86 | return g.greater(pout) ? [pout, g] : [g, pout]; 87 | } 88 | -------------------------------------------------------------------------------- /src/crypto/rsa/encrypt.test.ts: -------------------------------------------------------------------------------- 1 | import encrypt from './encrypt'; 2 | import { PredefinedKeys } from './keys'; 3 | 4 | test('rsa | encrypt', () => { 5 | const data = new Uint8Array([ 6 | 0xDB, 0x76, 0x1C, 0x27, 0x71, 0x8A, 0x23, 0x05, 0x04, 0x4F, 7 | 0x71, 0xF2, 0xAD, 0x95, 0x16, 0x29, 0xD7, 0x8B, 0x24, 0x49, 8 | ]); 9 | 10 | expect(encrypt(data, PredefinedKeys[0].n, PredefinedKeys[0].e)).toEqual( 11 | new Uint32Array([ 12 | 0xa4bd9732, 0xc5bf5fbf, 0x8d3bf557, 0x81f7d718, 0xa719d3cd, 0x4525ef80, 13 | 0xe9865963, 0xf1f48cdc, 0x44d8e3d4, 0xb508cc83, 0x9d01f5b5, 0x1c0724cc, 14 | 0x1ec70fbd, 0x738a4b6a, 0x2529cea0, 0x263133d9, 0xc5a7d6fc, 0xcba118d6, 15 | 0x2089d3d5, 0x7b035a37, 0xfe083399, 0xc00570d1, 0x0482e9da, 0xef324975, 16 | 0xeb4572a1, 0x0bafe2eb, 0x77a82043, 0x01ea8d04, 0x11ff66b6, 0xcb8104c8, 17 | 0xf596f989, 0xf95f381e, 0x970b8eaf, 0x22a84d43, 0x79617197, 0xe14227af, 18 | 0xdf452a5e, 0x151d89b0, 0x07177a4c, 0xef9b5d60, 0xd6c1da07, 0x8568dc0b, 19 | 0x10ec4cdb, 0xfbab81b9, 0x5c6f7f41, 0x4c6ba099, 0xa8c2d650, 0xe0f38ab0, 20 | 0x87518dc8, 0xbb922a61, 0xaa3593dd, 0x67f8cc28, 0x411588e7, 0x28d83a90, 21 | 0x62b901b6, 0x09073f11, 0xfa94ff2b, 0x8f9fd42d, 0x2892a5d5, 0xffbe2825, 22 | 0xa1bb0b92, 0x63a5e1d5, 0x3bff4f6e, 0x325d46f2, 23 | ]), 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /src/crypto/rsa/encrypt.ts: -------------------------------------------------------------------------------- 1 | const BigInt = require('big-integer'); // fix for rollup 2 | 3 | /** 4 | * Encrypts hex string with RSA 5 | * @param {string} data Data to encrypt, hex-string 6 | * @param {string} modulus RSA Key Modulus, hex-string 7 | * @param {string} exponent RSA Key Exponent, hex-string 8 | * @returns {string} Encrypted data, hex-string 9 | */ 10 | export default function encrypt(data: Uint8Array, modulus: Uint32Array, exponent: number): Uint32Array { 11 | const x = BigInt.fromArray(Array.from(data), 0x100); 12 | const n = BigInt.fromArray(Array.from(modulus), 0x100000000); 13 | const e = BigInt(exponent); 14 | 15 | return new Uint32Array(x.modPow(e, n).toArray(0x100000000).value); 16 | } 17 | -------------------------------------------------------------------------------- /src/crypto/rsa/keys.test.ts: -------------------------------------------------------------------------------- 1 | import { parseKey } from './keys'; 2 | 3 | test('rsa | keys', () => { 4 | const key = `-----BEGIN RSA PUBLIC KEY----- 5 | MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 6 | lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS 7 | an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw 8 | Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+ 9 | 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n 10 | Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 11 | -----END RSA PUBLIC KEY-----`; 12 | 13 | expect(parseKey(key)).toEqual({ 14 | fingerprint: 'c3b42b026ce86b21', 15 | n: new Uint32Array([ 16 | 0xc150023e, 0x2f70db79, 0x85ded064, 0x759cfecf, 0x0af328e6, 0x9a41daf4, 0xd6f01b53, 0x8135a6f9, 0x1f8f8b2a, 0x0ec9ba97, 0x20ce352e, 0xfcf6c568, 17 | 0x0ffc424b, 0xd6348649, 0x02de0b4b, 0xd6d49f4e, 0x580230e3, 0xae97d95c, 0x8b19442b, 0x3c0a10d8, 0xf5633fec, 0xedd6926a, 0x7f6dab0d, 0xdb7d457f, 18 | 0x9ea81b84, 0x65fcd6ff, 0xfeed1140, 0x11df91c0, 0x59caedaf, 0x97625f6c, 0x96ecc747, 0x25556934, 0xef781d86, 0x6b34f011, 0xfce4d835, 0xa090196e, 19 | 0x9a5f0e44, 0x49af7eb6, 0x97ddb907, 0x6494ca5f, 0x81104a30, 0x5b6dd276, 0x65722c46, 0xb60e5df6, 0x80fb16b2, 0x10607ef2, 0x17652e60, 0x236c255f, 20 | 0x6a28315f, 0x4083a967, 0x91d7214b, 0xf64c1df4, 0xfd0db194, 0x4fb26a2a, 0x57031b32, 0xeee64ad1, 0x5a8ba688, 0x85cde74a, 0x5bfc920f, 0x6abf59ba, 21 | 0x5c755063, 0x73e7130f, 0x9042da92, 0x2179251f, 22 | ]), 23 | e: 65537, 24 | }); 25 | 26 | const keyRSA = `-----BEGIN PUBLIC KEY----- 27 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB 28 | VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx 29 | hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd 30 | l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg 31 | gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O 32 | 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5 33 | JwIDAQAB 34 | -----END PUBLIC KEY-----`; 35 | 36 | expect(parseKey(keyRSA)).toEqual({ 37 | fingerprint: '0bc35f3509f7b7a5', 38 | n: new Uint32Array([ 39 | 0xaeec36c8, 0xffc109cb, 0x09962468, 0x5b978154, 0x15657bd7, 0x6d8c9c3e, 0x398103d7, 0xad16c9bb, 0xa6f525ed, 0x0412d7ae, 0x2c2de2b4, 0x4e77d72c, 40 | 0xbf4b7438, 0x709a4e64, 0x6a05c434, 0x27c7f184, 0xdebf7294, 0x7519680e, 0x65150089, 0x0c683279, 0x6dd11f77, 0x2c25ff8f, 0x576755af, 0xe055b0a3, 41 | 0x752c696e, 0xb7d8da0d, 0x8be1faf3, 0x8c9bdd97, 0xce0a77d3, 0x916230c4, 0x03216710, 0x0edd0f9e, 0x7a3a9b60, 0x2d04367b, 0x689536af, 0x0d64b613, 42 | 0xccba7962, 0x939d3b57, 0x682beb6d, 0xae5b6081, 0x30b2e52a, 0xca78ba02, 0x3cf6ce80, 0x6b1dc49c, 0x72cf928a, 0x7199d22e, 0x3d7ac84e, 0x47bc9427, 43 | 0xd0236945, 0xd10dbd15, 0x177bab41, 0x3fbf0edf, 0xda09f014, 0xc7a7da08, 0x8dde9759, 0x702ca760, 0xaf2b8e4e, 0x97cc055c, 0x617bd74c, 0x3d970086, 44 | 0x35b98dc4, 0xd621b489, 0x1da9fb04, 0x73047927, 45 | ]), 46 | e: 0x010001, 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/crypto/rsa/keys.ts: -------------------------------------------------------------------------------- 1 | import sha1 from '@cryptography/sha1'; 2 | import { Writer32, ab2i, Reader32 } from '../../serialization'; 3 | 4 | export type RSAKey = { 5 | fingerprint: string, 6 | n: Uint32Array, 7 | e: number, 8 | }; 9 | 10 | /** 11 | * Converts base64-encoded public key to {fingerprint, modulus, exponent} object 12 | */ 13 | export function parseKey(key: string): RSAKey { 14 | const matches = key.match(/-----BEGIN ([A-Z ]+?)-----([A-Za-z0-9\s+/]+)-----([A-Z ]+?)-----/m); 15 | 16 | if (!matches) throw new Error(`RSA Key: Unable to parse key \n ${key}`); 17 | 18 | const keyType = matches[1]; 19 | const buf = Uint8Array.from(atob(matches[2].trim()), (char) => char.charCodeAt(0)); 20 | 21 | let n: Uint8Array; let e: Uint8Array; 22 | 23 | switch (keyType) { 24 | case 'RSA PUBLIC KEY': 25 | n = buf.subarray(9, 265); 26 | e = buf.subarray(buf.byteLength - 3); 27 | break; 28 | 29 | case 'PUBLIC KEY': 30 | n = buf.subarray(33, 289); 31 | e = buf.subarray(buf.byteLength - 3); 32 | break; 33 | 34 | default: 35 | throw new Error(`RSA Key: Unknown key format ${keyType}`); 36 | } 37 | 38 | 39 | // rsa_public_key n:bytes e:bytes = RSAPublicKey 40 | const writer = new Writer32(new Uint32Array(66)); 41 | writer.bytes(n); 42 | writer.bytes(e); 43 | 44 | const keyHash = sha1(writer.buf); 45 | const reader = new Reader32(keyHash, 3); 46 | 47 | return { 48 | fingerprint: reader.long(), 49 | n: ab2i(n), 50 | e: (e[0] << 16) ^ (e[1] << 8) ^ e[0], 51 | }; 52 | } 53 | 54 | export const PredefinedKeys: RSAKey[] = [ 55 | /** 56 | * -----BEGIN RSA PUBLIC KEY----- 57 | * MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 58 | * lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS 59 | * an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw 60 | * Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+ 61 | * 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n 62 | * Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 63 | * -----END RSA PUBLIC KEY----- 64 | */ 65 | { 66 | fingerprint: 'c3b42b026ce86b21', 67 | n: new Uint32Array([ 68 | 0xc150023e, 0x2f70db79, 0x85ded064, 0x759cfecf, 0x0af328e6, 0x9a41daf4, 0xd6f01b53, 0x8135a6f9, 0x1f8f8b2a, 0x0ec9ba97, 0x20ce352e, 0xfcf6c568, 69 | 0x0ffc424b, 0xd6348649, 0x02de0b4b, 0xd6d49f4e, 0x580230e3, 0xae97d95c, 0x8b19442b, 0x3c0a10d8, 0xf5633fec, 0xedd6926a, 0x7f6dab0d, 0xdb7d457f, 70 | 0x9ea81b84, 0x65fcd6ff, 0xfeed1140, 0x11df91c0, 0x59caedaf, 0x97625f6c, 0x96ecc747, 0x25556934, 0xef781d86, 0x6b34f011, 0xfce4d835, 0xa090196e, 71 | 0x9a5f0e44, 0x49af7eb6, 0x97ddb907, 0x6494ca5f, 0x81104a30, 0x5b6dd276, 0x65722c46, 0xb60e5df6, 0x80fb16b2, 0x10607ef2, 0x17652e60, 0x236c255f, 72 | 0x6a28315f, 0x4083a967, 0x91d7214b, 0xf64c1df4, 0xfd0db194, 0x4fb26a2a, 0x57031b32, 0xeee64ad1, 0x5a8ba688, 0x85cde74a, 0x5bfc920f, 0x6abf59ba, 73 | 0x5c755063, 0x73e7130f, 0x9042da92, 0x2179251f, 74 | ]), 75 | e: 0x010001, 76 | }, 77 | 78 | /** ********************************************** */ 79 | 80 | /** -----BEGIN PUBLIC KEY----- 81 | * MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB 82 | * VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx 83 | * hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd 84 | * l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg 85 | * gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O 86 | * 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5 87 | * JwIDAQAB 88 | * -----END PUBLIC KEY----- */ 89 | { 90 | fingerprint: '0bc35f3509f7b7a5', 91 | n: new Uint32Array([ 92 | 0xaeec36c8, 0xffc109cb, 0x09962468, 0x5b978154, 0x15657bd7, 0x6d8c9c3e, 0x398103d7, 0xad16c9bb, 0xa6f525ed, 0x0412d7ae, 0x2c2de2b4, 0x4e77d72c, 93 | 0xbf4b7438, 0x709a4e64, 0x6a05c434, 0x27c7f184, 0xdebf7294, 0x7519680e, 0x65150089, 0x0c683279, 0x6dd11f77, 0x2c25ff8f, 0x576755af, 0xe055b0a3, 94 | 0x752c696e, 0xb7d8da0d, 0x8be1faf3, 0x8c9bdd97, 0xce0a77d3, 0x916230c4, 0x03216710, 0x0edd0f9e, 0x7a3a9b60, 0x2d04367b, 0x689536af, 0x0d64b613, 95 | 0xccba7962, 0x939d3b57, 0x682beb6d, 0xae5b6081, 0x30b2e52a, 0xca78ba02, 0x3cf6ce80, 0x6b1dc49c, 0x72cf928a, 0x7199d22e, 0x3d7ac84e, 0x47bc9427, 96 | 0xd0236945, 0xd10dbd15, 0x177bab41, 0x3fbf0edf, 0xda09f014, 0xc7a7da08, 0x8dde9759, 0x702ca760, 0xaf2b8e4e, 0x97cc055c, 0x617bd74c, 0x3d970086, 97 | 0x35b98dc4, 0xd621b489, 0x1da9fb04, 0x73047927, 98 | ]), 99 | e: 0x010001, 100 | }, 101 | 102 | /** ********************************************** */ 103 | 104 | /** -----BEGIN PUBLIC KEY----- 105 | * MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfLHfYH2r9R70w8prHbl 106 | * Wt/nDkh+XkgpflqQVcnAfSuTtO05lNPspQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOO 107 | * KPi0OfJXoRVylFzAQG/j83u5K3kRLbae7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ 108 | * 3TDS2pQOCtovG4eDl9wacrXOJTG2990VjgnIKNA0UMoP+KF03qzryqIt3oTvZq03 109 | * DyWdGK+AZjgBLaDKSnC6qD2cFY81UryRWOab8zKkWAnhw2kFpcqhI0jdV5QaSCEx 110 | * vnsjVaX0Y1N0870931/5Jb9ICe4nweZ9kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV 111 | * /wIDAQAB 112 | * -----END PUBLIC KEY----- */ 113 | { 114 | fingerprint: '15ae5fa8b5529542', 115 | n: new Uint32Array([ 116 | 0xbdf2c77d, 0x81f6afd4, 0x7bd30f29, 0xac76e55a, 0xdfe70e48, 0x7e5e4829, 0x7e5a9055, 0xc9c07d2b, 0x93b4ed39, 0x94d3eca5, 0x098bf18d, 0x978d54f8, 117 | 0xb7c713eb, 0x10247607, 0xe69af9ef, 0x44f38e28, 0xf8b439f2, 0x57a11572, 0x945cc040, 0x6fe3f37b, 0xb92b7911, 0x2db69eed, 0xf2dc7158, 0x4a661638, 118 | 0xea5becb9, 0xe2358507, 0x4b80d57d, 0x9f5710dd, 0x30d2da94, 0x0e0ada2f, 0x1b878397, 0xdc1a72b5, 0xce2531b6, 0xf7dd158e, 0x09c828d0, 0x3450ca0f, 119 | 0xf8a174de, 0xacebcaa2, 0x2dde84ef, 0x66ad370f, 0x259d18af, 0x80663801, 0x2da0ca4a, 0x70baa83d, 0x9c158f35, 0x52bc9158, 0xe69bf332, 0xa45809e1, 120 | 0xc36905a5, 0xcaa12348, 0xdd57941a, 0x482131be, 0x7b2355a5, 0xf4635374, 0xf3bd3ddf, 0x5ff925bf, 0x4809ee27, 0xc1e67d91, 0x20c5fe08, 0xa9de458b, 121 | 0x1b4a3c5d, 0x0a428437, 0xf2beca81, 0xf4e2d5ff, 122 | ]), 123 | e: 0x010001, 124 | }, 125 | 126 | /** ********************************************** */ 127 | 128 | /** -----BEGIN PUBLIC KEY----- 129 | * MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ditzm+mPND6xkhzwFI 130 | * z6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGrzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl 131 | * 4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+th6knSU0yLtNKuQVP6voMrnt9MV1X92L 132 | * GZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvSUwwc+yi1/gGaybwlzZwqXYoPOhwMebzK 133 | * Uk0xW14htcJrRrq+PXXQbRzTMynseCoPIoke0dtCodbA3qQxQovE16q9zz4Otv2k 134 | * 4j63cz53J+mhkVWAeWxVGI0lltJmWtEYK6er8VqqWot3nqmWMXogrgRLggv/Nbbo 135 | * oQIDAQAB 136 | * -----END PUBLIC KEY----- */ 137 | { 138 | fingerprint: 'aeae98e13cd7f94f', 139 | n: new Uint32Array([ 140 | 0xb3f762b7, 0x39be98f3, 0x43eb1921, 0xcf0148cf, 0xa27ff7af, 0x02b64712, 0x13fed9da, 0xa0098976, 0xe6677503, 0x24f1abce, 0xa4c31e43, 0xb7d11f15, 141 | 0x79133f2b, 0x3d9fe274, 0x74e46205, 0x8884e5e1, 0xb123be9c, 0xbbc6a443, 0xb2925c08, 0x520e7325, 0xe6f1a6d5, 0x0e117eb6, 0x1ea49d25, 0x34c8bb4d, 142 | 0x2ae4153f, 0xabe832b9, 0xedf4c575, 0x5fdd8b19, 0x940b81d1, 0xd96cf433, 0xd19e6a22, 0x968a85dc, 0x80f0312f, 0x596bd253, 0x0c1cfb28, 0xb5fe019a, 143 | 0xc9bc25cd, 0x9c2a5d8a, 0x0f3a1c0c, 0x79bcca52, 0x4d315b5e, 0x21b5c26b, 0x46babe3d, 0x75d06d1c, 0xd33329ec, 0x782a0f22, 0x891ed1db, 0x42a1d6c0, 144 | 0xdea43142, 0x8bc4d7aa, 0xbdcf3e0e, 0xb6fda4e2, 0x3eb7733e, 0x7727e9a1, 0x91558079, 0x6c55188d, 0x2596d266, 0x5ad1182b, 0xa7abf15a, 0xaa5a8b77, 145 | 0x9ea99631, 0x7a20ae04, 0x4b820bff, 0x35b6e8a1, 146 | ]), 147 | e: 0x010001, 148 | }, 149 | 150 | /** ********************************************** */ 151 | 152 | /** -----BEGIN PUBLIC KEY----- 153 | * MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q0 154 | * 5shjg8/4p6047bn6/m8yPy1RBsvIyvuDuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xb 155 | * nfxL5BXHplJhMtADXKM9bWB11PU1Eioc3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA 156 | * 9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvifRLJbY08/Gp66KpQvy7g8w7VB8wlgePe 157 | * xW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqePji9NP3tJUFQjcECqcm0yV7/2d0t/pbC 158 | * m+ZH1sadZspQCEPPrtbkQBlvHb4OLiIWPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6M 159 | * AQIDAQAB 160 | * -----END PUBLIC KEY----- */ 161 | { 162 | fingerprint: '5a181b2235057d98', 163 | n: new Uint32Array([ 164 | 0xbe6a7155, 0x8ee577ff, 0x03023cfa, 0x17aab4e6, 0xc86383cf, 0xf8a7ad38, 0xedb9fafe, 0x6f323f2d, 0x5106cbc8, 0xcafb83b8, 0x69cffd1c, 0xcf121cd7, 165 | 0x43d509e5, 0x89e68765, 0xc96601e8, 0x13dc5b9d, 0xfc4be415, 0xc7a65261, 0x32d0035c, 0xa33d6d60, 0x75d4f535, 0x122a1cdf, 0xe017041f, 0x1088d141, 166 | 0x9f65c8e5, 0x490ee613, 0xe16dbf66, 0x2698c0f5, 0x4870f047, 0x5fa893fc, 0x41eb55b0, 0x8ff1ac21, 0x1bc045de, 0xd31be27d, 0x12c96d8d, 0x3cfc6a7a, 167 | 0xe8aa50bf, 0x2ee0f30e, 0xd507cc25, 0x81e3dec5, 0x6de94f5d, 0xc0a7abee, 0x0be990b8, 0x93f2887b, 0xd2c6310a, 0x1e0a9e3e, 0x38bd34fd, 0xed254150, 168 | 0x8dc102a9, 0xc9b4c95e, 0xffd9dd2d, 0xfe96c29b, 0xe647d6c6, 0x9d66ca50, 0x0843cfae, 0xd6e44019, 0x6f1dbe0e, 0x2e22163c, 0x61ca48c7, 0x9116fa77, 169 | 0x21672674, 0x9a976a1c, 0x4b0944b5, 0x121e8c01, 170 | ]), 171 | e: 0x010001, 172 | }, 173 | ]; 174 | 175 | /** 176 | * Find RSA key by fingerprint 177 | */ 178 | export function getKeyByFingerprints(fingerprints: string[]): RSAKey { 179 | for (let i = 0; i < fingerprints.length; i += 1) { 180 | const item = fingerprints[i]; 181 | for (let j = 0; j < PredefinedKeys.length; j += 1) { 182 | if (PredefinedKeys[j].fingerprint === item) { 183 | return PredefinedKeys[j]; 184 | } 185 | } 186 | } 187 | 188 | return PredefinedKeys[0]; 189 | } 190 | -------------------------------------------------------------------------------- /src/crypto/srp.test.ts: -------------------------------------------------------------------------------- 1 | import { genPasswordSRP } from './srp'; 2 | import { salt1, salt2, g, p, srpId, rand, srpB, password, A, M1 } from '../mock/srp'; 3 | 4 | test('async | getPasswordKdf', () => { 5 | const res = genPasswordSRP(salt1, salt2, g, p, srpId, srpB, password, rand); 6 | 7 | expect(res.srp_id).toEqual(srpId); 8 | expect(res.A).toEqual(A); 9 | expect(res.M1).toEqual(M1); 10 | }); 11 | -------------------------------------------------------------------------------- /src/crypto/srp.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable newline-per-chained-call */ 2 | import sha256 from '@cryptography/sha256'; 3 | import pbkdf2 from '@cryptography/pbkdf2'; 4 | import sha512 from '@cryptography/sha512'; 5 | import { ab2i, randomize, i2ab } from '../serialization'; 6 | import { InputCheckPasswordSRP } from '../tl'; 7 | 8 | type BigInteger = import('big-integer').BigInteger; 9 | const BigInt = require('big-integer'); // fix for rollup 10 | 11 | function uintTo64(src: Uint8Array): Uint32Array { 12 | const buf = new Uint32Array(64); 13 | 14 | for (let i = src.length - 1, j = 63; i >= 0 && j >= 0; i -= 4, j--) { 15 | buf[j] = ( 16 | src[i - 3] << 24 17 | ^ src[i - 2] << 16 18 | ^ src[i - 1] << 8 19 | ^ src[i] 20 | ); 21 | } 22 | 23 | return buf; 24 | } 25 | 26 | function bintTo64(src: BigInteger): Uint32Array { 27 | const srcBuf = src.toArray(0x100000000).value; 28 | 29 | if (srcBuf.length === 64) return new Uint32Array(srcBuf); 30 | 31 | const buf = new Uint32Array(64); 32 | 33 | for (let i = buf.length, j = srcBuf.length; i >= 0 && j >= 0; i--, j--) buf[i] = srcBuf[j]; 34 | 35 | return buf; 36 | } 37 | 38 | export function genPasswordSRP( 39 | salt1: Uint8Array, salt2: Uint8Array, cg: number, cp: Uint8Array, srpId: string, csrpB: Uint8Array, password: string, rand?: Uint32Array, 40 | ): InputCheckPasswordSRP.inputCheckPasswordSRP { 41 | let clientSaltString = ''; 42 | for (let i = 0; i < salt1.length; i++) clientSaltString += String.fromCharCode(salt1[i]); 43 | 44 | const clientSalt = ab2i(salt1); 45 | const serverSalt = ab2i(salt2); 46 | const g = BigInt(cg); 47 | const p = BigInt.fromArray(Array.from(cp), 0x100); 48 | 49 | const gBuf = new Uint32Array(64); 50 | const pBuf = uintTo64(cp); 51 | 52 | gBuf[63] = 3; 53 | 54 | const srpB = BigInt.fromArray(Array.from(csrpB), 0x100); 55 | const srpBBuf = uintTo64(csrpB); 56 | 57 | let pwdhash = sha256(clientSaltString + password + clientSaltString); 58 | pwdhash = sha256.stream().update(serverSalt).update(pwdhash).update(serverSalt).digest(); 59 | pwdhash = pbkdf2(pwdhash, clientSaltString, 100000, sha512, 64); 60 | pwdhash = sha256.stream().update(serverSalt).update(pwdhash).update(serverSalt).digest(); 61 | 62 | const x = BigInt.fromArray(Array.from(pwdhash), 0x100000000); 63 | const gx = g.modPow(x, p); 64 | 65 | const k = BigInt.fromArray(Array.from(sha256.stream().update(pBuf).update(gBuf).digest()), 0x100000000); 66 | const kgx = k.multiply(gx).mod(p); 67 | 68 | if (!rand) { 69 | rand = new Uint32Array(6); 70 | randomize(rand); 71 | } 72 | 73 | const a = BigInt.fromArray(Array.from(rand), 0x100000000); 74 | const Ac = g.modPow(a, p); 75 | const AcBuf = bintTo64(Ac); 76 | 77 | let bkgx = srpB.subtract(kgx); 78 | if (bkgx.lesser(BigInt.zero)) bkgx = bkgx.add(p); 79 | 80 | const u = BigInt.fromArray(Array.from(sha256.stream().update(AcBuf).update(srpBBuf).digest()), 0x100000000); 81 | const ux = u.multiply(x); 82 | const uxa = ux.add(a); 83 | 84 | const S = bkgx.modPow(uxa, p); 85 | const SBuf = bintTo64(S); 86 | 87 | const K = sha256(SBuf); 88 | const h1 = sha256(pBuf); 89 | const h2 = sha256(gBuf); 90 | 91 | for (let i = 0; i < h1.length; i++) h1[i] ^= h2[i]; 92 | 93 | const M1 = sha256.stream() 94 | .update(h1) 95 | .update(sha256(clientSalt)) 96 | .update(sha256(serverSalt)) 97 | .update(AcBuf) 98 | .update(srpBBuf) 99 | .update(K) 100 | .digest(); 101 | 102 | return { 103 | _: 'inputCheckPasswordSRP', 104 | srp_id: srpId, 105 | A: i2ab(AcBuf), 106 | M1: i2ab(M1), 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const data: Record; 3 | export default data; 4 | } 5 | 6 | declare module 'pako/lib/deflate' { 7 | export { 8 | deflate, Deflate, deflateRaw, gzip, 9 | } from 'pako'; 10 | } 11 | declare module 'pako/lib/inflate' { 12 | export { 13 | inflate, Inflate, inflateRaw, ungzip, 14 | } from 'pako'; 15 | } 16 | 17 | declare module 'aes-js' 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Client } from './client'; 2 | 3 | /** Type Shortcuts */ 4 | export type Transports = import('./client/types').Transports; 5 | export type ClientError = import('./client/types').ClientError; 6 | export type AuthKey = import('./client/types').AuthKey; 7 | export type TransportState = import('./transport/abstract').TransportState; 8 | export type MethodDeclMap = import('./tl').MethodDeclMap; 9 | export type UpdateDeclMap = import('./tl').UpdateDeclMap; 10 | 11 | export * from './tl/layer113/types'; 12 | -------------------------------------------------------------------------------- /src/message/encrypted.test.ts: -------------------------------------------------------------------------------- 1 | import EncryptedMessage from './encrypted'; 2 | import { Reader32, Writer32 } from '../serialization'; 3 | 4 | test('message | encrypted parse', () => { 5 | const data = new Uint32Array([ 6 | 0xc18c59d4, 0xd3b6809d, 0x15f5716b, 0xaed3fca1, 0x2432ef57, 0x54dab007, 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 7 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 8 | ]); 9 | 10 | const msg = new EncryptedMessage(data); 11 | 12 | expect(msg.authKey).toBe('9d80b6d3d4598cc1'); 13 | expect(msg.key).toEqual(data.subarray(2, 6)); 14 | 15 | expect(msg.reader instanceof Reader32).toBeTruthy(); 16 | expect(msg.writer instanceof Writer32).toBeTruthy(); 17 | }); 18 | 19 | test('message | encrypted create', () => { 20 | const data = new Uint32Array([ 21 | 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 22 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 23 | ]); 24 | 25 | const msg = new EncryptedMessage(data, true); 26 | 27 | msg.authKey = '9d80b6d3d4598cc1'; 28 | msg.key = new Uint32Array([0x15f5716b, 0xaed3fca1, 0x2432ef57, 0x54dab007]); 29 | 30 | expect(msg.buf).toEqual( 31 | new Uint32Array([ 32 | 0xc18c59d4, 0xd3b6809d, 0x15f5716b, 0xaed3fca1, 0x2432ef57, 0x54dab007, 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 33 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 34 | ]), 35 | ); 36 | }); 37 | 38 | test('message | decrypt', () => { 39 | const data = new Uint32Array([ 40 | 0x505e0d0a, 0x1e4b12fc, 0xfe178abb, 0xebbbf1f7, 0xe501af6e, 0x0338c1cc, 0x59d44475, 0xd70ea76a, 0xd28f9a8b, 0x87e64462, 0x05775720, 41 | 0x03c2bf22, 0xcb69e57f, 0xb1947956, 0xefc04e63, 0xc4e2f1a4, 0xea5f500d, 0x3282deb3, 0x39d6617a, 0x8192be5a, 0x3de91fc2, 0xa6c674d0, 42 | 0x20222aff, 0x6bbeefaa, 0x0940371d, 0x48cf10d7, 0x29d36731, 0x03fb05f2, 0x9df3a0c5, 0xf1ffd183, 0x7e8ef1a5, 0x3405adb9, 0x79a3e171, 43 | 0x69b9a8e5, 0xc751784b, 0x74b35475, 0xd4e7cd28, 0x626344c2, 0x776cf070, 0x34334947, 0x7bd3a145, 0x206e07eb, 44 | ]); 45 | 46 | const key = new Uint32Array([ 47 | 0x56a0f05d, 0x8df1b0ff, 0x4d3c1751, 0x5f3f3d13, 0x3a17717a, 0xa053a709, 0xbb30db9f, 0x62ddac65, 0x91950fb5, 0xc3e042aa, 0x5988f11a, 48 | 0xa81a874a, 0xacf09679, 0xc5efa787, 0xb08e1ba4, 0x16cc5f00, 0x010cc765, 0x393e9379, 0xa2ad8abf, 0x30db102c, 0x0d78551b, 0x4a34bf41, 49 | 0x35788e24, 0x48855bea, 0x978b48db, 0xd901c143, 0xe5b53ce3, 0x8eee7a31, 0x556f5f20, 0xf440e55a, 0xa0829128, 0x3b768cf8, 0x1e49153e, 50 | 0x45f79b9b, 0x9bbae28d, 0xe7cedae5, 0xce470273, 0xd4ee4dca, 0x4bce3d42, 0x549d2c9a, 0x4e6a4e02, 0xf12a52fc, 0x85e7de2e, 0x2dca4283, 51 | 0x8f1e368b, 0x3e0121f0, 0x63ff020e, 0xdb084666, 0x0e802b56, 0x5c2b4fc5, 0x87496cf3, 0x4bee4465, 0x2e24da74, 0x00b191bf, 0x23573d54, 52 | 0x354bc978, 0x4a9aefd2, 0x92476fd5, 0x97b7dd3a, 0xf5d390e1, 0x132f6f4d, 0xe9cd411d, 0x2ec89274, 0x60ccba5b, 53 | ]); 54 | 55 | const msg = new EncryptedMessage(data); 56 | 57 | expect(msg.decrypt(key).buf).toEqual( 58 | new Uint32Array([ 59 | 0x8df55eb2, 0xff2bfb31, 0xdfe57cf2, 0xc92a629c, 0x015cda87, 0x3537245e, 0x02000000, 0x58000000, 0xdcf8f173, 0x02000000, 0x0174d987, 60 | 0x3537245e, 0x01000000, 0x1c000000, 0x0809c29e, 0x04c5500c, 0x3537245e, 0xfed9f752, 0xcceb4c98, 0x8df55eb2, 0xff2bfb31, 0x0154da87, 61 | 0x3537245e, 0x02000000, 0x14000000, 0x59b4d662, 0x15c4b51c, 0x01000000, 0x04c5500c, 0x3537245e, 0xd06c04d8, 0xc6caac8f, 0x3cc6b63f, 62 | 0x1ba9f484, 0xff48da89, 0xf5d4591d, 63 | ]), 64 | ); 65 | }); 66 | 67 | test('message | encrypted array buffer', () => { 68 | const data = new Uint32Array([ 69 | 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 70 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 71 | ]); 72 | 73 | const msg = new EncryptedMessage(data); 74 | const buffer = msg.arrayBuffer; 75 | 76 | expect(buffer.byteLength).toEqual(data.byteLength); 77 | expect(new Uint8Array(buffer)[0]).toBe(0xd9); 78 | expect(new Uint8Array(buffer)[buffer.byteLength - 1]).toBe(0x8b); 79 | }); 80 | -------------------------------------------------------------------------------- /src/message/encrypted.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | import sha256 from '@cryptography/sha256'; 3 | import { IGE } from '@cryptography/aes'; 4 | // eslint-disable-next-line import/no-cycle 5 | import Message from './message'; 6 | import { Writer32, Reader32, i2ab } from '../serialization'; 7 | 8 | /** 9 | * MessageEncrypted is a buffer with 24 byte padding, which has been encrypted. 10 | * Ref: https://core.telegram.org/mtproto/description#encrypted-message 11 | */ 12 | export default class EncryptedMessage { 13 | /** Byte data source of message */ 14 | buf: Uint32Array; 15 | 16 | _writer: Writer32; 17 | _reader: Reader32; 18 | 19 | /** Length of message headers */ 20 | hlen = 6; 21 | 22 | /** If message is content related */ 23 | isContentRelated = true; 24 | 25 | constructor(src: Uint32Array, shouldWrap = false) { 26 | if (shouldWrap) { 27 | this.buf = new Uint32Array(this.hlen + src.length); 28 | this._writer = new Writer32(this.buf); 29 | 30 | for (let i = 0; i < src.length; i++) this.buf[this.hlen + i] = src[i]; 31 | } else { 32 | this.buf = src; 33 | this._writer = new Writer32(this.buf); 34 | } 35 | 36 | this._reader = new Reader32(this.buf); 37 | } 38 | 39 | /** 40 | * Method sets authKey to the first 8 bytes 41 | */ 42 | set authKey(authKeyID: string) { 43 | this._writer.pos = 0; 44 | this._writer.long(authKeyID); 45 | } 46 | 47 | /** 48 | * Method gets authKey to the first 8 bytes 49 | */ 50 | get authKey(): string { 51 | this._reader.pos = 0; 52 | return this._reader.long(); 53 | } 54 | 55 | /** 56 | * Method sets 8-24 bytes with msg_key 57 | */ 58 | set key(msgKey: Uint32Array) { 59 | this._writer.pos = 2; 60 | this._writer.int128(msgKey); 61 | } 62 | 63 | /** 64 | * Method gets hex string from 8-24 bytes 65 | */ 66 | get key(): Uint32Array { 67 | this._reader.pos = 2; 68 | return this._reader.int128(); 69 | } 70 | 71 | /** 72 | * Method gets data payload 73 | */ 74 | get data(): Uint32Array { 75 | return this.buf.subarray(6); 76 | } 77 | 78 | /** 79 | * Method gets writer for buffer 80 | */ 81 | get writer(): Writer32 { 82 | this._writer.pos = this.hlen; 83 | return this._writer; 84 | } 85 | 86 | /** 87 | * Method gets reader for buffer 88 | */ 89 | get reader(): Reader32 { 90 | this._reader.pos = this.hlen; 91 | return this._reader; 92 | } 93 | 94 | /** 95 | * Decrypts MessageEncrypted object with AES-256-IGE mode. 96 | * https://core.telegram.org/mtproto/description#protocol-description 97 | */ 98 | decrypt(key: Uint32Array): Message { 99 | const msgKey = this.key; 100 | const sha256a = sha256.stream().update(msgKey).update(key.subarray(2, 11)).digest(); 101 | const sha256b = sha256.stream().update(key.slice(12, 21)).update(msgKey).digest(); 102 | 103 | const a2 = sha256a[2]; 104 | const a3 = sha256a[3]; 105 | const a4 = sha256a[4]; 106 | const a5 = sha256a[5]; 107 | 108 | for (let i = 2; i < 6; i++) sha256a[i] = sha256b[i]; 109 | 110 | sha256b[2] = a2; 111 | sha256b[3] = a3; 112 | sha256b[4] = a4; 113 | sha256b[5] = a5; 114 | 115 | const cipher = new IGE(sha256a, sha256b); 116 | return new Message(cipher.decrypt(this.data)); 117 | } 118 | 119 | get arrayBuffer(): ArrayBuffer | SharedArrayBuffer { 120 | return i2ab(this.buf); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/message/error.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import ErrorMessage from './error'; 3 | 4 | test('message | error message create', () => { 5 | const err = new ErrorMessage(0xfffffe53); 6 | expect(err.error.code).toBe(404); 7 | }); 8 | -------------------------------------------------------------------------------- /src/message/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error message is a specail class for handling error packets. 3 | */ 4 | type ErrorDetails = { 5 | code: number, 6 | message: string, 7 | }; 8 | 9 | export default class ErrorMessage { 10 | /** Packet hex to error message */ 11 | static Errors: Record = { 12 | 0xfffffe53: { 13 | code: 404, 14 | message: 'Invalid packet was sent', 15 | }, 16 | 0xfffffe6c: { 17 | code: -1, 18 | message: 'Invalid auth key', 19 | }, 20 | }; 21 | 22 | /** Details of received error */ 23 | error: ErrorDetails; 24 | 25 | constructor(err: number) { 26 | this.error = ErrorMessage.Errors[err]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/message/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable import/no-cycle */ 3 | export { default as Message } from './message'; 4 | export { default as MessageV1 } from './message.v1'; 5 | export { default as PlainMessage } from './plain'; 6 | export { default as EncryptedMessage } from './encrypted'; 7 | export { default as ErrorMessage } from './error'; 8 | export { default as bytesToMessage } from './resolve'; 9 | -------------------------------------------------------------------------------- /src/message/message.test.ts: -------------------------------------------------------------------------------- 1 | import Message from './message'; 2 | import { Reader32, Writer32 } from '../serialization'; 3 | 4 | 5 | test('message | message create', () => { 6 | const payload = new Uint32Array([ 7 | 0x63241605, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 0xA5CF4D33, 0xF4A11EA8, 0x77BA4AA5, 0x73907330, 0x0817ED48, 8 | 0x941A08F9, 0x81000000, 0x15C4B51C, 0x01000000, 0x216BE86C, 0x022BB4C3, 9 | ]); 10 | 11 | const salt = '0102030405060708'; 12 | const sid = 'fffefdfcfbfaf9f8'; 13 | const seq = 111; 14 | const msgid = '51e57ac42770964a'; 15 | 16 | const msg = new Message(payload, true); 17 | msg.id = msgid; 18 | msg.salt = salt; 19 | msg.seqNo = seq; 20 | msg.sessionID = sid; 21 | msg.len(); 22 | 23 | expect(msg.buf.length).toBeGreaterThan(8 + payload.length); 24 | expect(msg.buf.length % 4).toBe(0); 25 | 26 | const reader = new Reader32(msg.buf); 27 | 28 | expect(reader.long()).toBe(salt); 29 | expect(reader.long()).toBe(sid); 30 | expect(reader.long()).toBe(msgid); 31 | expect(reader.int32()).toBe(seq); 32 | expect(reader.int32()).toBe(payload.length * 4); 33 | expect(msg.data).toEqual(payload); 34 | 35 | expect(msg.id).toBe(msgid); 36 | expect(msg.seqNo).toBe(seq); 37 | 38 | expect(msg.reader instanceof Reader32).toBeTruthy(); 39 | expect(msg.writer instanceof Writer32).toBeTruthy(); 40 | }); 41 | 42 | test('message | message encrypt', () => { 43 | const key = new Uint32Array([ 44 | 0x12f3e1fc, 0xeeaf1761, 0x5e74203d, 0xab70feb2, 0xb63f3523, 0x927a15de, 0xe19d42d3, 0x3c59fb2a, 0xc7241193, 0x56912674, 45 | 0x264af9cf, 0xb19574f7, 0xf4dcfaa0, 0xf7094ade, 0x8eaf1e3e, 0x317aeab5, 0xdaf543c7, 0xf06b8c95, 0x0e45884e, 0xf2837e8d, 46 | 0xc3d7dba9, 0xce5c41c0, 0x78f1d7b6, 0x5c394434, 0x8e8fcb3a, 0x0342eaec, 0x27138e13, 0x66da4509, 0x35b6ca2a, 0x5f6ded45, 47 | 0xc73fcd13, 0x771ea100, 0x4be21b39, 0xa9935621, 0xa8b559c7, 0x411e8fa6, 0x271ec445, 0x5c2975b1, 0x5fa65957, 0x5c29bec7, 48 | 0x18d46288, 0xdafdddef, 0xdd8f0360, 0x46bec48a, 0x0fbae62a, 0x3cc5c76c, 0xbb07e609, 0x21c3c943, 0xa8bf0606, 0x3efa421d, 49 | 0x9eecd692, 0x409a5f3e, 0x3829c9a8, 0x43cc891e, 0xd131e26b, 0x1afcd31f, 0x48578579, 0xe37255d2, 0x17e26415, 0x1bd47c81, 50 | 0xae22d928, 0xf20e0f01, 0x7aadedd7, 0x19b92ac1, 51 | ]); 52 | 53 | const keyid = '9d80b6d3d4598cc1'; 54 | 55 | const message = new Message(new Uint32Array([0x3212D579, 0xEE35452E, 0xD23E0D0C, 0x92841AA7, 0xD31B2E9B, 0xDEF2151E, 0x80D15860, 0x311C85DB])); 56 | 57 | expect(message.encrypt(key, keyid).buf).toEqual( 58 | new Uint32Array([ 59 | 0xc18c59d4, 0xd3b6809d, 0x15f5716b, 0xaed3fca1, 0x2432ef57, 0x54dab007, 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 60 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 61 | ]), 62 | ); 63 | }); 64 | 65 | test('message | message generate id', () => { 66 | expect(Message.GenerateID().length).toBe(16); 67 | }); 68 | -------------------------------------------------------------------------------- /src/message/message.ts: -------------------------------------------------------------------------------- 1 | import sha256 from '@cryptography/sha256'; 2 | import { IGE } from '@cryptography/aes'; 3 | import PlainMessage from './plain'; 4 | // eslint-disable-next-line 5 | import EncryptedMessage from './encrypted'; 6 | import { Writer32, Reader32, randomize } from '../serialization'; 7 | import { parse } from '../tl'; 8 | 9 | /** 10 | * Message is a buffer with 32 byte padding, which should be encrypted. 11 | * Ref: https://core.telegram.org/mtproto/description#encrypted-message-encrypted-data 12 | */ 13 | export default class Message { 14 | /** Byte data source of message */ 15 | buf: Uint32Array; 16 | 17 | _writer: Writer32; 18 | _reader: Reader32; 19 | 20 | /** Length of message headers */ 21 | hlen = 8; 22 | 23 | /** Padding length */ 24 | plen: number = 0; 25 | 26 | /** If message is content related */ 27 | isContentRelated = true; 28 | 29 | /** 30 | * Creates new Bytes object from: 31 | * - Uint32Array 32 | */ 33 | constructor(src: Uint32Array, shouldWrap: boolean = false) { 34 | if (shouldWrap) { 35 | this.plen = this.getPaddingLen(src.length); 36 | this.buf = new Uint32Array(this.hlen + src.length + this.plen); 37 | this._writer = new Writer32(this.buf); 38 | 39 | this.len(); 40 | this.padding(); 41 | for (let i = 0; i < src.length; i++) this.buf[this.hlen + i] = src[i]; 42 | } else { 43 | this.buf = src; 44 | this._writer = new Writer32(this.buf); 45 | } 46 | 47 | this._reader = new Reader32(this.buf); 48 | } 49 | 50 | getPaddingLen(len: number) { 51 | return 8 - (len % 4); 52 | } 53 | 54 | // // eslint-disable-next-line 55 | // getPaddingLen(len: number) { 56 | // return 32 - (len % 16); // + Math.floor(Math.random() * 20) * 16; 57 | // } 58 | 59 | /** 60 | * Method sets message identificator it to the 16-24 bytes 61 | */ 62 | set id(id: string) { 63 | this._writer.pos = 4; 64 | this._writer.long(id); 65 | } 66 | 67 | /** 68 | * Method gets message identificator from the 16-24 bytes 69 | */ 70 | get id(): string { 71 | this._reader.pos = 4; 72 | return this._reader.long(); 73 | } 74 | 75 | /** 76 | * Method sets 28-32 bytes with message_data_length 77 | */ 78 | len(): void { 79 | this._writer.pos = 7; 80 | this._writer.int32((this.buf.length - this.hlen - this.plen) * 4); 81 | } 82 | 83 | get dataLength(): number { 84 | this._reader.pos = 7; 85 | return this._reader.int32() / 4; 86 | } 87 | 88 | /** 89 | * Method sets first 8 bytes with salt header 90 | */ 91 | set salt(salt: string) { 92 | this._writer.pos = 0; 93 | this._writer.long(salt); 94 | } 95 | 96 | /** 97 | * Method sets second 8-16 bytes with session_id header 98 | */ 99 | set sessionID(sid: string) { 100 | this._writer.pos = 2; 101 | this._writer.long(sid); 102 | } 103 | 104 | /** 105 | * Method sets 24-28 bytes with seq_no header 106 | */ 107 | set seqNo(seq: number) { 108 | this._writer.pos = 6; 109 | this._writer.int32(seq); 110 | } 111 | 112 | /** 113 | * Method gets 24-28 bytes with seq_no header 114 | */ 115 | get seqNo(): number { 116 | this._reader.pos = 6; 117 | return this._reader.int32(); 118 | } 119 | 120 | /** 121 | * Method gets writer for buffer 122 | */ 123 | get writer(): Writer32 { 124 | this._writer.pos = this.hlen; 125 | return this._writer; 126 | } 127 | 128 | /** 129 | * Method gets reader for buffer 130 | */ 131 | get reader(): Reader32 { 132 | this._reader.pos = this.hlen; 133 | return this._reader; 134 | } 135 | 136 | /** 137 | * Method gets encrypted_data starts 32 byte 138 | */ 139 | get data(): Uint32Array { 140 | return this.buf.subarray(this.hlen, this.hlen + this.dataLength); 141 | } 142 | 143 | /** 144 | * Method sets padding bytes with random data 145 | */ 146 | padding() { 147 | randomize(this.buf, this.buf.length - this.plen); 148 | } 149 | 150 | // for debug purpose 151 | get parsed(): any { 152 | return parse(this.reader); 153 | } 154 | 155 | /** 156 | * Generates unique message identificator depending on current time 157 | */ 158 | static GenerateID(): string { 159 | return PlainMessage.GenerateID(); 160 | } 161 | 162 | /** 163 | * Encrypts MessageData object with AES-256-IGE mode. 164 | * https://core.telegram.org/mtproto/description#protocol-description 165 | */ 166 | encrypt(key: Uint32Array, authKeyID: string): EncryptedMessage { 167 | const msgKeyLarge = sha256.stream().update(key.subarray(22, 30)).update(this.buf).digest(); 168 | const msgKey = msgKeyLarge.subarray(2, 6); 169 | const sha256a = sha256.stream().update(msgKey).update(key.subarray(0, 9)).digest(); 170 | const sha256b = sha256.stream().update(key.subarray(10, 19)).update(msgKey).digest(); 171 | 172 | const a2 = sha256a[2]; 173 | const a3 = sha256a[3]; 174 | const a4 = sha256a[4]; 175 | const a5 = sha256a[5]; 176 | 177 | for (let i = 2; i < 6; i++) sha256a[i] = sha256b[i]; 178 | 179 | sha256b[2] = a2; 180 | sha256b[3] = a3; 181 | sha256b[4] = a4; 182 | sha256b[5] = a5; 183 | 184 | const cipher = new IGE(sha256a, sha256b); 185 | const encrypted = cipher.encrypt(this.buf); 186 | const encMsg = new EncryptedMessage(encrypted, true); 187 | 188 | encMsg.authKey = authKeyID; 189 | encMsg.key = msgKey; 190 | 191 | return encMsg; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/message/message.v1.test.ts: -------------------------------------------------------------------------------- 1 | import MessageV1 from './message.v1'; 2 | 3 | test('message.v1 | encrypt', () => { 4 | const keyid = 'dad68f09be2ea6e5'; 5 | const key = new Uint32Array([ 6 | 0x1634f1e1, 0x213cf354, 0x131aa0f6, 0x64a11125, 0xd323cf45, 0x22cceffa, 0x8d892b5e, 0x0bed9147, 0xcd496c1a, 0x4dbd50bf, 0x68c2e099, 0x15faa791, 7 | 0x1a903fba, 0xeb82bada, 0xd313d231, 0x340da176, 0xd4295840, 0x21adece0, 0xe56466c2, 0xd612d09c, 0xb569fb0a, 0xfc26605f, 0x70f1c41e, 0x41d93a19, 8 | 0xae9d244f, 0x75723711, 0x13c056ea, 0xd341bd6b, 0x29b5207d, 0xfd98d0fc, 0x46459054, 0xaae92ac2, 0x8949aeab, 0x01d374e7, 0xe2f7b290, 0x6c485d63, 9 | 0xf99bf14c, 0xd51e3a23, 0xe5c63eb5, 0x8cc0e3b4, 0x587a829c, 0x2fbbde45, 0xa0f6f54d, 0x2f2909cc, 0xe8d1dc31, 0xb62b499b, 0x11753523, 0xae1a6087, 10 | 0x3b9eeee2, 0x3beaa13e, 0x985222cd, 0x008b5346, 0xd8101893, 0x646916c7, 0x220b74d7, 0x1039823d, 0xdeb43721, 0x43d7347c, 0x1592e167, 0xc5ca74a0, 11 | 0xe9c64f90, 0x8d0a02ed, 0x09ece490, 0xb4c53cde, 12 | ]); 13 | 14 | const data = new Uint32Array([ 15 | 0x2aff4d29, 0x73bea7f1, 0x0842c521, 0x8f982e48, 0x04a6600a, 0xf338245e, 0x00000000, 0x28000000, 0x65f7a375, 0xb26b3fe0, 0x1918dcd5, 0x68e75982, 16 | 0xa8306df4, 0xe5a62ebe, 0x098fd6da, 0xaf2aa7a1, 0xbc7fadda, 0x437f245e, 0xb157d6a9, 0xa552c25e, 17 | ]); 18 | 19 | const message = new MessageV1(data); 20 | 21 | expect(message.encrypt(key, keyid).buf).toEqual( 22 | new Uint32Array([ 23 | 0xe5a62ebe, 0x098fd6da, 0xf8ebfa77, 0x4c049b4e, 0x8ae76898, 0x6e905194, 0x976bd960, 0xae5ad3d8, 0xdc67740f, 0xe1df05de, 0x5e0c5864, 0xd9f96f64, 24 | 0xad5a4819, 0x3017219f, 0x528c543e, 0x29f5a318, 0x7517b126, 0x5be1ec64, 0x67290b7f, 0xdbae0a01, 0x6fe44c25, 0x9e21dd05, 0x2c71fbaa, 0x09145ed6, 25 | 0x99c1f268, 0x58385bc2, 26 | ]), 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/message/message.v1.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring, newline-per-chained-call */ 2 | import sha1 from '@cryptography/sha1'; 3 | import { IGE } from '@cryptography/aes'; 4 | import Message from './message'; 5 | import EncryptedMessage from './encrypted'; 6 | 7 | export default class MessageV1 extends Message { 8 | getPaddingLen(len: number) { 9 | return 4 - (len % 4); 10 | } 11 | 12 | /** 13 | * MTProto v1.0 14 | * Encrypts MessageData object with AES-256-IGE mode. 15 | * https://core.telegram.org/mtproto/description_v1 16 | */ 17 | encrypt(key: Uint32Array, keyid: string): EncryptedMessage { 18 | const msgKeyLarge = sha1(this.buf.subarray(0, this.hlen + this.dataLength)); 19 | const msgKey = msgKeyLarge.subarray(1, 5); 20 | 21 | const sha1a = sha1.stream().update(msgKey).update(key.subarray(0, 8)).digest(); 22 | const sha1b = sha1.stream().update(key.subarray(8, 12)).update(msgKey).update(key.subarray(12, 16)).digest(); 23 | const sha1c = sha1.stream().update(key.subarray(16, 24)).update(msgKey).digest(); 24 | const sha1d = sha1.stream().update(msgKey).update(key.subarray(24, 32)).digest(); 25 | 26 | const aesKey = new Uint32Array(8); 27 | for (let i = 0; i < 2; i++) aesKey[i] = sha1a[i]; 28 | for (let i = 2; i < 5; i++) aesKey[i] = sha1b[i]; 29 | for (let i = 5; i < 8; i++) aesKey[i] = sha1c[i - 4]; 30 | 31 | const aesIv = new Uint32Array(8); 32 | for (let i = 0; i < 3; i++) aesIv[i] = sha1a[i + 2]; 33 | for (let i = 3; i < 5; i++) aesIv[i] = sha1b[i - 3]; 34 | for (let i = 6; i < 8; i++) aesIv[i] = sha1d[i - 6]; 35 | aesIv[5] = sha1c[4]; 36 | 37 | const cipher = new IGE(aesKey, aesIv); 38 | const encrypted = cipher.encrypt(this.buf); 39 | const encMsg = new EncryptedMessage(encrypted, true); 40 | 41 | encMsg.authKey = keyid; 42 | encMsg.key = msgKey; 43 | 44 | return encMsg; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/message/plain.test.ts: -------------------------------------------------------------------------------- 1 | import PlainMessage from './plain'; 2 | import { Reader32, Writer32 } from '../serialization'; 3 | 4 | test('message | plain create', () => { 5 | const payload = new Uint32Array([0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC]); 6 | 7 | const msg = new PlainMessage(payload, true); 8 | msg.id = '51e57ac42770964a'; 9 | 10 | expect(msg.nonce).toEqual(new Uint32Array([0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC])); 11 | 12 | expect(msg.buf).toEqual( 13 | new Uint32Array([ 14 | 0x00000000, 0x00000000, 0x4A967027, 0xC47AE551, 0x14000000, 15 | 0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 16 | ]), 17 | ); 18 | 19 | expect(msg.writer instanceof Writer32).toBeTruthy(); 20 | }); 21 | 22 | test('message | plain generate id', () => { 23 | for (let i = 0; i < 20; i++) PlainMessage.GenerateID(); 24 | 25 | expect(PlainMessage.GenerateID().length).toBe(16); 26 | expect(PlainMessage.GenerateID().length).toBe(16); 27 | expect(PlainMessage.GenerateID().length).toBe(16); 28 | }); 29 | 30 | test('message | plain read', () => { 31 | const message = new Uint32Array([ 32 | 0x00000000, 0x00000000, 0x4A967027, 0xC47AE551, 0x14000000, 33 | 0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 34 | ]); 35 | 36 | const msg = new PlainMessage(message); 37 | 38 | expect(msg.id).toBe('51e57ac42770964a'); 39 | expect(msg.nonce).toEqual(new Uint32Array([0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC])); 40 | expect(msg.data).toEqual(new Uint32Array([0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC])); 41 | 42 | expect(msg.reader instanceof Reader32).toBeTruthy(); 43 | expect(msg.reader.int32()).toBe(0x60469778); 44 | }); 45 | 46 | test('message | plain array buffer', () => { 47 | const message = new Uint32Array([ 48 | 0x00000000, 0x00000000, 0x4A967027, 0xC47AE551, 0x14000000, 49 | 0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 50 | ]); 51 | 52 | const msg = new PlainMessage(message); 53 | const buffer = msg.arrayBuffer; 54 | 55 | expect(buffer.byteLength).toEqual(message.byteLength); 56 | expect(new Uint8Array(buffer)[0]).toBe(0x00); 57 | expect(new Uint8Array(buffer)[buffer.byteLength - 1]).toBe(0xFC); 58 | }); 59 | -------------------------------------------------------------------------------- /src/message/plain.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-mixed-operators */ 2 | import { Writer32, Reader32, i2h, i2ab } from '../serialization'; 3 | import { parse } from '../tl'; 4 | 5 | let lastGeneratedLo = 0; 6 | let lastGeneratedHi = 0; 7 | 8 | /** 9 | * MessagePlain is a buffer with 20 byte padding, which should not be encrypted. 10 | * Ref: https://core.telegram.org/mtproto/description#unencrypted-message 11 | */ 12 | export default class PlainMessage { 13 | /** Byte data source of message */ 14 | buf: Uint32Array; 15 | 16 | _writer: Writer32; 17 | _reader: Reader32; 18 | 19 | /** Length of message headers */ 20 | hlen = 5; 21 | 22 | /** Message nonce */ 23 | nonce: Uint32Array; 24 | 25 | /** 26 | * Creates new Bytes object from: 27 | * - AraryBuffer 28 | * - TLConstructor 29 | */ 30 | constructor(src: Uint32Array, shouldWrap: boolean = false) { 31 | this.buf = src; 32 | 33 | if (shouldWrap) { 34 | this.buf = new Uint32Array(this.hlen + src.length); 35 | this._writer = new Writer32(this.buf); 36 | this.len(); 37 | 38 | for (let i = 0; i < src.length; i++) this.buf[this.hlen + i] = src[i]; 39 | } else { 40 | this.buf = src; 41 | this._writer = new Writer32(this.buf); 42 | } 43 | 44 | this._reader = new Reader32(this.buf); 45 | this._reader.pos = 6; 46 | this.nonce = this._reader.int128(); 47 | } 48 | 49 | /** 50 | * Method sets message identificator it to the 8-16 bytes 51 | */ 52 | set id(id: string) { 53 | this._writer.pos = 2; 54 | this._writer.long(id); 55 | } 56 | 57 | /** 58 | * Method gets message identificator from the 8-16 bytes 59 | */ 60 | get id(): string { 61 | this._reader.pos = 2; 62 | return this._reader.long(); 63 | } 64 | 65 | /** 66 | * Method sets 16-20 bytes with message_data_length 67 | */ 68 | len(): void { 69 | this._writer.pos = 4; 70 | this._writer.int32((this.buf.length - this.hlen) * 4); 71 | } 72 | 73 | 74 | /** 75 | * Method gets writer for buffer 76 | */ 77 | get writer(): Writer32 { 78 | this._writer.pos = this.hlen; 79 | return this._writer; 80 | } 81 | 82 | /** 83 | * Method gets reader for buffer 84 | */ 85 | get reader(): Reader32 { 86 | this._reader.pos = this.hlen; 87 | return this._reader; 88 | } 89 | 90 | /** 91 | * Method gets encrypted_data starts 32 byte 92 | */ 93 | get data(): Uint32Array { 94 | return this.buf.subarray(this.hlen); 95 | } 96 | 97 | // for debug purpose 98 | get parsed(): any { 99 | return parse(this.reader); 100 | } 101 | 102 | /** 103 | * Generates unique message identificator depending on current time 104 | */ 105 | static GenerateID(): string { 106 | const time = Date.now(); 107 | const nanosecond = Math.floor(time % 1000); 108 | const second = Math.floor(time / 1000); 109 | 110 | let generatedHi = second; 111 | let generatedLo = (nanosecond << 20 | nanosecond << 8 | 4); 112 | 113 | // avoid collisions 114 | if (lastGeneratedHi > generatedHi || (lastGeneratedHi === generatedHi && lastGeneratedLo >= generatedLo)) { 115 | generatedHi = lastGeneratedHi; 116 | generatedLo = lastGeneratedLo + 4; 117 | } 118 | 119 | lastGeneratedHi = generatedHi; 120 | lastGeneratedLo = generatedLo; 121 | 122 | return i2h(generatedHi) + i2h(generatedLo); 123 | } 124 | 125 | get arrayBuffer(): ArrayBuffer | SharedArrayBuffer { 126 | return i2ab(this.buf); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/message/resolve.test.ts: -------------------------------------------------------------------------------- 1 | import PlainMessage from './plain'; 2 | import ErrorMessage from './error'; 3 | import EncryptedMessage from './encrypted'; 4 | import bytesToMessage from './resolve'; 5 | import plainMock from '../mock/message_plain'; 6 | import errorMock from '../mock/message_error'; 7 | import encryptedMock from '../mock/message_encrypted'; 8 | 9 | test('message | resolve', () => { 10 | expect(bytesToMessage(errorMock) instanceof ErrorMessage).toBeTruthy(); 11 | expect(bytesToMessage(plainMock.buf) instanceof PlainMessage).toBeTruthy(); 12 | expect(bytesToMessage(encryptedMock.buf) instanceof EncryptedMessage).toBeTruthy(); 13 | 14 | let raised = 0; 15 | 16 | try { 17 | bytesToMessage(new Uint32Array([0xFFFFFFFF])); 18 | } catch (e) { 19 | expect(e.message.length).toBeGreaterThan(0); 20 | raised++; 21 | } 22 | 23 | expect(raised).toBe(1); 24 | }); 25 | -------------------------------------------------------------------------------- /src/message/resolve.ts: -------------------------------------------------------------------------------- 1 | import ErrorMessage from './error'; 2 | import EncryptedMessage from './encrypted'; 3 | import PlainMessage from './plain'; 4 | import { Reader32 } from '../serialization'; 5 | 6 | export default function bytesToMessage(data: Uint32Array): ErrorMessage | EncryptedMessage | PlainMessage { 7 | const reader = new Reader32(data); 8 | 9 | // error message 10 | if (data.length <= 1) { 11 | const error = reader.int32(); 12 | 13 | if (ErrorMessage.Errors[error]) { 14 | return new ErrorMessage(error); 15 | } 16 | 17 | throw new Error(`Unexpected message: ${error.toString(16)}`); 18 | } 19 | 20 | // quick ack 21 | // if (data.length <= 3) { 22 | // throw new Error('Unexpected quick_ack message'); 23 | // } 24 | 25 | // plain message 26 | if (data[0] === 0) { 27 | return new PlainMessage(data); 28 | } 29 | 30 | return new EncryptedMessage(data); 31 | } 32 | -------------------------------------------------------------------------------- /src/mock/auth_key.ts: -------------------------------------------------------------------------------- 1 | import { AuthKey, AuthKeyNotNull } from '../client/types'; 2 | 3 | export const permanentKey: AuthKeyNotNull = { 4 | key: new Uint32Array(32), 5 | id: 'permID', 6 | }; 7 | 8 | export const temporaryKey: AuthKeyNotNull = { 9 | key: new Uint32Array(32), 10 | id: 'tempID', 11 | expires: 12345679, 12 | binded: false, 13 | }; 14 | 15 | export const nullKey: AuthKey = null; 16 | 17 | export default permanentKey; 18 | -------------------------------------------------------------------------------- /src/mock/client.ts: -------------------------------------------------------------------------------- 1 | import { ClientInterface, defaultClientConfig } from '../client/types'; 2 | import dc from './client_dc_service'; 3 | import updates from './client_updates'; 4 | 5 | const ClientMock: ClientInterface = { 6 | cfg: defaultClientConfig, 7 | dc, 8 | updates, 9 | plainCall: () => {}, 10 | call: () => {}, 11 | send: () => {}, 12 | authorize: () => {}, 13 | }; 14 | 15 | export default ClientMock; 16 | -------------------------------------------------------------------------------- /src/mock/client_dc_service.ts: -------------------------------------------------------------------------------- 1 | import DCService from '../client/dc'; 2 | import metaMock from './client_meta'; 3 | 4 | export default new DCService(metaMock); 5 | -------------------------------------------------------------------------------- /src/mock/client_meta.ts: -------------------------------------------------------------------------------- 1 | import { ClientMetaData } from '../client/types'; 2 | 3 | export default { 4 | baseDC: 2, 5 | pfs: true, 6 | dcs: {}, 7 | } as ClientMetaData; 8 | -------------------------------------------------------------------------------- /src/mock/client_updates.ts: -------------------------------------------------------------------------------- 1 | import UpdateService from '../client/updates'; 2 | 3 | export default new UpdateService(); 4 | -------------------------------------------------------------------------------- /src/mock/message_encrypted.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedMessage } from '../message'; 2 | 3 | export default new EncryptedMessage( 4 | new Uint32Array([ 5 | 0xc18c59d4, 0xd3b6809d, 0x15f5716b, 0xaed3fca1, 0x2432ef57, 0x54dab007, 0xd9913153, 0xe4d91685, 0x223c44de, 0xd7038708, 6 | 0x4792d25e, 0xabc00fed, 0x0552ac04, 0x09930e8b, 7 | ]), 8 | ); 9 | -------------------------------------------------------------------------------- /src/mock/message_error.ts: -------------------------------------------------------------------------------- 1 | export default new Uint32Array([0x53feffff]); 2 | -------------------------------------------------------------------------------- /src/mock/message_plain.ts: -------------------------------------------------------------------------------- 1 | import { PlainMessage } from '../message'; 2 | 3 | export default new PlainMessage( 4 | new Uint32Array([ 5 | 0x00000000, 0x00000000, 0x4A967027, 0xC47AE551, 0x14000000, 0x78974660, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 6 | ]), 7 | ); 8 | -------------------------------------------------------------------------------- /src/mock/srp.ts: -------------------------------------------------------------------------------- 1 | export const salt1 = new Uint8Array([0xb6, 0xcb, 0x41, 0xe4, 0x2a, 0x56, 0x05, 0x4e, 0xb2, 0xe8, 0x32, 0x0e, 0x30, 0xb2, 0x15, 0xc0]); 2 | 3 | export const salt2 = new Uint8Array([0xf7, 0x95, 0x1e, 0x1b, 0x4a, 0xc1, 0xd1, 0xff, 0x67, 0x22, 0xd4, 0xad, 0xf1, 0x94, 0x15, 0x77]); 4 | 5 | export const g = 3; 6 | 7 | export const p = new Uint8Array([ 8 | 0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x04, 0x8e, 0x6c, 0x52, 0x2f, 0x70, 0xf1, 0x3f, 0x73, 0x98, 0x0d, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1, 9 | 0x49, 0x34, 0xd0, 0x37, 0x56, 0x3d, 0x93, 0x0f, 0x48, 0x19, 0x8a, 0x0a, 0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb, 10 | 0xfa, 0x33, 0x6f, 0x6e, 0x0a, 0xc9, 0x25, 0x13, 0x95, 0x43, 0xae, 0xd4, 0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, 0x70, 0x5a, 11 | 0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64, 12 | 0x24, 0x77, 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a, 0xc8, 0xcc, 0x49, 0x88, 0x07, 0x08, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f, 13 | 0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, 0x6b, 0x0f, 0x6b, 0x41, 14 | 0x0d, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, 0x4e, 0x0e, 0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0x0d, 0x59, 0x65, 0xb4, 15 | 0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4, 0x65, 0xb0, 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4, 16 | 0xe4, 0x18, 0xfc, 0x15, 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff, 0x5e, 0xed, 0x70, 0x05, 0x0d, 0xed, 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59, 17 | 0xd9, 0x56, 0x85, 0x0c, 0xe9, 0x29, 0x85, 0x1f, 0x0d, 0x81, 0x15, 0xf6, 0x35, 0xb1, 0x05, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, 0x54, 0xbf, 18 | 0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x04, 0x03, 0x11, 0x9c, 0xd8, 0xe3, 0xb9, 0x2f, 0xcc, 0x5b, 19 | ]); 20 | 21 | export const srpId = '3541879028763065224'; 22 | 23 | export const srpB = new Uint8Array([ 24 | 0x9a, 0xc2, 0xcd, 0x7f, 0x80, 0x94, 0x5b, 0x9d, 0xb2, 0x77, 0x37, 0x55, 0x3d, 0xf8, 0xe4, 0xda, 0xca, 0xa1, 0xcd, 0x1e, 0x18, 0x79, 0x3d, 0xbc, 25 | 0x75, 0xd5, 0x34, 0x31, 0x1e, 0xc7, 0x9a, 0x00, 0x23, 0x4b, 0xec, 0xae, 0x65, 0x5b, 0x58, 0xca, 0x5e, 0xf2, 0x2a, 0x34, 0xd3, 0x89, 0x8f, 0xb1, 26 | 0x81, 0x9c, 0xa9, 0x8c, 0x3d, 0x0d, 0xaf, 0x93, 0xec, 0x90, 0xea, 0x11, 0x56, 0xa7, 0x65, 0x22, 0xc8, 0xa5, 0x3e, 0x94, 0xa1, 0x03, 0xa9, 0xcc, 27 | 0x67, 0x33, 0x25, 0xf1, 0xc3, 0xd6, 0x20, 0xf9, 0x67, 0x8c, 0x04, 0x0f, 0x3c, 0x26, 0xc9, 0xc4, 0x09, 0x15, 0x8b, 0xea, 0x37, 0xb5, 0x00, 0x53, 28 | 0x85, 0x2c, 0x6c, 0x08, 0x31, 0x86, 0x9d, 0x94, 0x0b, 0x7e, 0x69, 0x09, 0xf2, 0x00, 0xad, 0xfc, 0xd7, 0x5d, 0x0b, 0x6f, 0xe9, 0x11, 0x92, 0x62, 29 | 0x91, 0x99, 0x09, 0xac, 0xf7, 0x7f, 0x9a, 0xbe, 0x0f, 0x68, 0xbe, 0x3a, 0xaa, 0x5c, 0x6a, 0xbc, 0xb3, 0xa7, 0x11, 0x5f, 0x65, 0x50, 0x1f, 0xb4, 30 | 0xeb, 0xcf, 0x3f, 0x49, 0x50, 0x6e, 0x00, 0xf9, 0x2c, 0x33, 0x5a, 0x5b, 0x3b, 0x82, 0x77, 0x40, 0x54, 0x1b, 0x46, 0x66, 0xdd, 0x9e, 0xe6, 0x64, 31 | 0x54, 0x19, 0xab, 0xfd, 0xa6, 0x7a, 0xf4, 0x6b, 0x34, 0x95, 0x9e, 0x40, 0x10, 0x82, 0xfc, 0x0a, 0x8e, 0x72, 0x64, 0xc0, 0xce, 0xed, 0xfc, 0xbc, 32 | 0x0c, 0x8a, 0x72, 0x36, 0x71, 0x29, 0x4e, 0x05, 0x2c, 0xa0, 0xed, 0xcd, 0xdc, 0x9a, 0x6b, 0xc9, 0xf5, 0xe8, 0xb4, 0x88, 0x67, 0xd0, 0x4f, 0x3b, 33 | 0x12, 0x28, 0x83, 0x22, 0x86, 0x80, 0x94, 0x6a, 0xa9, 0xcb, 0xcb, 0x0a, 0x3f, 0xea, 0x6e, 0x61, 0x8e, 0x50, 0x99, 0x06, 0x2e, 0x36, 0xca, 0x99, 34 | 0x11, 0x84, 0x61, 0x56, 0xe9, 0x9f, 0x5b, 0x2c, 0x03, 0xb4, 0x1d, 0xe3, 0xde, 0x2c, 0x95, 0x39, 35 | ]); 36 | 37 | export const rand = new Uint32Array([ 38 | 0x9153faef, 0x8f2bb6da, 0x91f6e5bc, 0x96bc0086, 0x0a530a57, 0x2a0f45aa, 0xc0842b46, 0x02d711f8, 0xbda8d59f, 0xb53705e4, 0xae3e31a3, 0xc4f06819, 39 | 0x55425f22, 0x4297b8e9, 0xefd898fe, 0xc22046de, 0xbb7ba8a0, 0xbcf2be1a, 0xda7b1004, 0x24ea318f, 0xdcef6ccf, 0xe6d7ab7d, 0x978c0eb7, 0x6a807d4a, 40 | 0xb200eb76, 0x7a22de0d, 0x828bc53f, 0x42c5a35c, 0x2df6e6ce, 0xeef9a348, 0x7aae8e9e, 0xf2271f2f, 0x6742e83b, 0x8211161f, 0xb1a0e037, 0x491ab2c2, 41 | 0xc73ad63c, 0x8bd1d739, 0xde1b523f, 0xe8d46127, 0x0cedcf24, 0x0de8da75, 0xf31be493, 0x35765329, 0x55041dc5, 0x770c18d3, 0xe75d0b35, 0x7df9da4a, 42 | 0x5c8726d4, 0xfced87d1, 0x57524008, 0x83dc57fa, 0x1937ac17, 0x608c5446, 0xc4774dcd, 0x123676d6, 0x83ce3a1a, 0xb9f7e020, 0xca52faaf, 0xc9996982, 43 | 0x2717c8e0, 0x7ea383d5, 0xfb1a007b, 0xa0d170cb, 44 | ]); 45 | 46 | export const password = 'pas'; 47 | 48 | export const A = new Uint8Array([ 49 | 0x53, 0x27, 0xbf, 0x0e, 0x6b, 0x28, 0xd0, 0x75, 0x7f, 0xa2, 0xab, 0x23, 0x9c, 0xaf, 0x74, 0x2c, 0x72, 0x68, 0x90, 0x95, 0xc5, 0xdc, 0xef, 0x0f, 50 | 0x54, 0x29, 0xe5, 0x1a, 0x3c, 0xa4, 0x29, 0x99, 0xaa, 0x9b, 0x48, 0x2e, 0x72, 0x0f, 0xd9, 0xa2, 0x4b, 0xe9, 0x93, 0x1a, 0xa5, 0x41, 0x06, 0x0d, 51 | 0xf2, 0xf0, 0x80, 0x06, 0xd9, 0xc4, 0xec, 0x58, 0x7a, 0x43, 0x4d, 0x87, 0xab, 0x2a, 0x5e, 0xf6, 0x1d, 0x45, 0xe9, 0x79, 0x39, 0x57, 0xe6, 0xd0, 52 | 0xde, 0xaf, 0x0c, 0x79, 0x1e, 0xba, 0xef, 0x55, 0xd9, 0xf9, 0xc1, 0xf4, 0xec, 0xfc, 0x8d, 0x57, 0x26, 0x6c, 0x45, 0x97, 0x04, 0x80, 0x5e, 0xa9, 53 | 0x24, 0x37, 0xcd, 0x83, 0x9d, 0xc8, 0x20, 0x8c, 0xe9, 0xfa, 0xb5, 0x2b, 0xf9, 0xbd, 0x61, 0x40, 0xe8, 0x27, 0x83, 0xb5, 0x2c, 0xc9, 0xb6, 0x32, 54 | 0xcc, 0x3c, 0x7a, 0x5f, 0x35, 0x6b, 0x25, 0x28, 0x11, 0x14, 0xfc, 0xcb, 0xcf, 0xf4, 0x3b, 0xb7, 0x99, 0xa6, 0xcd, 0xd6, 0xf6, 0x68, 0x75, 0x3c, 55 | 0x26, 0xa3, 0x5f, 0x94, 0x55, 0x18, 0x68, 0x75, 0xad, 0x46, 0xb0, 0x45, 0xe4, 0x89, 0x05, 0xa0, 0xa0, 0xc9, 0xc8, 0xf5, 0x64, 0xfa, 0x99, 0x80, 56 | 0x60, 0xd8, 0xf3, 0xdb, 0x49, 0x76, 0x92, 0x13, 0xb0, 0x49, 0xe0, 0x4a, 0xd8, 0x30, 0x75, 0x0e, 0x50, 0x5d, 0x1e, 0xcb, 0xda, 0xcd, 0x5b, 0xd3, 57 | 0x20, 0x91, 0xf6, 0x59, 0x19, 0xb8, 0xa0, 0x85, 0xe3, 0x31, 0x5e, 0xfc, 0xa2, 0xe1, 0xa5, 0xeb, 0x41, 0xa4, 0x1d, 0xef, 0xbb, 0x63, 0xed, 0x39, 58 | 0xc4, 0x6c, 0x99, 0x19, 0x7f, 0x44, 0xc5, 0x3b, 0x01, 0x12, 0x4b, 0xdd, 0xab, 0xf1, 0xf6, 0x98, 0x24, 0x57, 0xf5, 0x3d, 0xc9, 0xd3, 0x47, 0x7a, 59 | 0x6e, 0xb7, 0x45, 0x04, 0x6c, 0x70, 0x95, 0x20, 0x82, 0x5c, 0x57, 0x1a, 0x63, 0xb7, 0xdf, 0xa1, 60 | ]).buffer; 61 | 62 | export const M1 = new Uint8Array([ 63 | 0x86, 0x47, 0xf0, 0xc3, 0x81, 0x4d, 0xcb, 0xa9, 0xfc, 0xe5, 0x3b, 0xa4, 0x0d, 0x87, 0x92, 0x08, 0x80, 0xa4, 0x13, 0xed, 0xdc, 0xcc, 0x59, 0x95, 64 | 0xa2, 0x89, 0xf6, 0xe0, 0x8c, 0x21, 0x51, 0x2c, 65 | ]).buffer; 66 | -------------------------------------------------------------------------------- /src/mock/transport_config.ts: -------------------------------------------------------------------------------- 1 | import { Transports } from 'client/types'; 2 | 3 | export default { 4 | test: true, 5 | debug: false, 6 | ssl: true, 7 | dc: 2, 8 | host: 'venus.web.telegram.org', 9 | thread: 1, 10 | transport: 'websocket' as Transports, 11 | APILayer: 113, 12 | APIID: 1194894, 13 | APIHash: 'a4aed71c0c88a9db8eccb29e9a1d90f3', 14 | 15 | deviceModel: 'test', 16 | systemVersion: 'test', 17 | appVersion: '1.0', 18 | langCode: 'en', 19 | }; 20 | -------------------------------------------------------------------------------- /src/serialization/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Reader32 } from './reader'; 2 | export { default as Writer32 } from './writer'; 3 | export { randomize, reverse32, i2h, i2ab, ab2i, isBigEndian } from './utils'; 4 | -------------------------------------------------------------------------------- /src/serialization/reader.test.ts: -------------------------------------------------------------------------------- 1 | import Reader32 from './reader'; 2 | 3 | test('serialization | reader32 res_pq', () => { 4 | const buffer = new Uint32Array([ 5 | 0x00000000, 0x00000000, 0x01C8831E, 0xC97AE551, 0x40000000, 0x63241605, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 0xA5CF4D33, 6 | 0xF4A11EA8, 0x77BA4AA5, 0x73907330, 0x0817ED48, 0x941A08F9, 0x81000000, 0x15C4B51C, 0x01000000, 0x216BE86C, 0x022BB4C3, 7 | ]); 8 | 9 | const reader = new Reader32(buffer); 10 | 11 | expect(reader.long()).toBe('00'.repeat(8)); 12 | expect(reader.long()).toBe('51e57ac91e83c801'); 13 | expect(reader.int32()).toBe(64); 14 | expect(reader.int32()).toBe(0x05162463); 15 | expect(reader.int128()).toEqual(new Uint32Array([0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC])); 16 | expect(reader.int128()).toEqual(new Uint32Array([0xA5CF4D33, 0xF4A11EA8, 0x77BA4AA5, 0x73907330])); 17 | expect(reader.bytes()).toEqual(new Uint8Array([0x17, 0xED, 0x48, 0x94, 0x1A, 0x08, 0xF9, 0x81]).buffer); 18 | expect(reader.int32()).toBe(0x1cb5c415); 19 | expect(reader.int32()).toBe(1); 20 | expect(reader.long()).toBe('c3b42b026ce86b21'); 21 | }); 22 | 23 | test('serialization | reader32 string', () => { 24 | const buffer = new Uint32Array([ 25 | 0xfe2c0100, 0x4c6f7265, 0x6d206970, 0x73756d20, 0x646f6c6f, 0x72207369, 0x7420616d, 0x65742c20, 0x636f6e73, 0x65637465, 0x74757220, 26 | 0x61646970, 0x69736369, 0x6e672065, 0x6c69742e, 0x20536564, 0x20636f6e, 0x67756520, 0x64696374, 0x756d2065, 0x6e696d20, 0x65676574, 27 | 0x20636f6e, 0x76616c6c, 0x69732e20, 0x51756973, 0x71756520, 0x696d7065, 0x72646965, 0x7420636f, 0x6e76616c, 0x6c697320, 0x72697375, 28 | 0x73206e65, 0x63207665, 0x6e656e61, 0x7469732e, 0x20536564, 0x20616c69, 0x71756574, 0x20706861, 0x72657472, 0x6120706f, 0x72747469, 29 | 0x746f722e, 0x20437572, 0x61626974, 0x75722065, 0x66666963, 0x69747572, 0x20696163, 0x756c6973, 0x20746f72, 0x746f7220, 0x6574206c, 30 | 0x6163696e, 0x69612e20, 0x41656e65, 0x616e2063, 0x6f6e7661, 0x6c6c6973, 0x20697073, 0x756d2063, 0x6f6d6d6f, 0x646f2065, 0x6c656d65, 31 | 0x6e74756d, 0x20706f72, 0x74612e20, 0x436c6173, 0x73206170, 0x74656e74, 0x20746163, 0x69746920, 0x706f7375, 0x6572652e, 0x40000000, 32 | ]); 33 | 34 | const reader = new Reader32(buffer); 35 | 36 | expect(reader.string()).toBe( 37 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed congue dictum enim eget convallis. Quisque imperdiet convallis risus nec venenatis. ' 38 | + 'Sed aliquet pharetra porttitor. Curabitur efficitur iaculis tortor et lacinia. Aenean convallis ipsum commodo elementum porta. Class aptent ' 39 | + 'taciti posuere.', 40 | ); 41 | 42 | expect(reader.int32()).toBe(0x40); 43 | }); 44 | 45 | test('serialization | reader32 int256', () => { 46 | const buffer = new Uint32Array([ 47 | 0x73756d20, 0x646f6c6f, 0x72207369, 0x7420616d, 48 | 0x65742c20, 0x636f6e73, 0x65637465, 0x74757220, 49 | ]); 50 | 51 | const reader = new Reader32(buffer); 52 | 53 | expect(reader.int256()).toEqual(buffer); 54 | }); 55 | 56 | test('serialization | reader32 bool', () => { 57 | const buffer = new Uint32Array([ 58 | 0xb5757299, 0x37bc7997, 59 | ]); 60 | 61 | const reader = new Reader32(buffer); 62 | 63 | expect(reader.bool()).toEqual(true); 64 | expect(reader.bool()).toEqual(false); 65 | }); 66 | 67 | test('serialization | reader32 int64', () => { 68 | const buffer = new Uint32Array([ 69 | 0x3E054982, 0x8CCA27E9, 70 | ]); 71 | 72 | const reader = new Reader32(buffer); 73 | 74 | expect(reader.int64()).toEqual('3e0549828cca27e9'); 75 | }); 76 | 77 | // test('serialization | reader32 double', () => { 78 | // const buffer = new Uint32Array([ 79 | // 0x3F880000, 0x00000000, 80 | // ]); 81 | 82 | // const reader = new Reader32(buffer); 83 | 84 | // expect(reader.double()).toEqual(1.72325e-319); 85 | // }); 86 | -------------------------------------------------------------------------------- /src/serialization/reader.ts: -------------------------------------------------------------------------------- 1 | import { i2h, utf8decoder } from './utils'; 2 | 3 | /** 4 | * Type Language Deserialization 5 | */ 6 | export default class Reader32 { 7 | private buf: Uint32Array; 8 | pos: number; 9 | 10 | constructor(buf: Uint32Array, start: number = 0) { 11 | this.buf = buf; 12 | this.pos = start; 13 | } 14 | 15 | int32() { 16 | const uint = this.buf[this.pos++]; 17 | 18 | return ( 19 | (uint >>> 24) 20 | ^ ((uint >> 16) & 0xFF) << 8 21 | ^ ((uint >> 8) & 0xFF) << 16 22 | ^ (uint & 0xFF) << 24 23 | ) >>> 0; 24 | } 25 | 26 | int64() { 27 | return i2h(this.buf[this.pos++]) + i2h(this.buf[this.pos++]); 28 | } 29 | 30 | long() { 31 | const lo = this.int32(); 32 | const hi = this.int32(); 33 | return i2h(hi) + i2h(lo); 34 | } 35 | 36 | int128() { 37 | this.pos += 4; 38 | return this.buf.subarray(this.pos - 4, this.pos); 39 | } 40 | 41 | int256() { 42 | this.pos += 8; 43 | return this.buf.subarray(this.pos - 8, this.pos); 44 | } 45 | 46 | double() { 47 | this.pos += 2; 48 | 49 | const int8 = new Uint8Array([ 50 | this.buf[this.pos - 2] >>> 24, 51 | (this.buf[this.pos - 2] >> 16) & 0xFF, 52 | (this.buf[this.pos - 2] >> 8) & 0xFF, 53 | (this.buf[this.pos - 2]) & 0xFF, 54 | this.buf[this.pos - 1] >>> 24, 55 | (this.buf[this.pos - 1] >> 16) & 0xFF, 56 | (this.buf[this.pos - 1] >> 8) & 0xFF, 57 | (this.buf[this.pos - 1]) & 0xFF, 58 | ]); 59 | 60 | return new Float64Array(int8.buffer)[0]; 61 | } 62 | 63 | bytes() { 64 | let buffer; 65 | let int32 = this.buf[this.pos++]; 66 | let len = int32 >>> 24; 67 | let i = 0; 68 | 69 | if (len >= 0xFE) { 70 | int32 &= 0xFFFFFF; 71 | len = int32 >>> 16 72 | ^ ((int32 >> 8) & 0xFF) << 8 73 | ^ (int32 & 0xFF) << 16; 74 | 75 | buffer = new Uint8Array(len); 76 | } else { 77 | buffer = new Uint8Array(len); 78 | buffer[i++] = (int32 >> 16) & 0xFF; 79 | buffer[i++] = (int32 >> 8) & 0xFF; 80 | buffer[i++] = (int32) & 0xFF; 81 | } 82 | 83 | for (; i < len; i += 4) { 84 | int32 = this.buf[this.pos++]; 85 | buffer[i] = int32 >>> 24; 86 | buffer[i + 1] = (int32 >> 16) & 0xFF; 87 | buffer[i + 2] = (int32 >> 8) & 0xFF; 88 | buffer[i + 3] = int32 & 0xFF; 89 | } 90 | 91 | return buffer.buffer; 92 | } 93 | 94 | string() { 95 | return utf8decoder.decode(this.bytes()); 96 | } 97 | 98 | bool() { 99 | return this.int32() === 0x997275b5; 100 | } 101 | 102 | rollback() { 103 | this.pos--; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/serialization/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { reverse32, i2abLow } from './utils'; 2 | 3 | test('serialization | reverse32', () => { 4 | const buffer = new Uint32Array([0x01020304, 0x05060708]); 5 | 6 | expect(reverse32(buffer)).toEqual(new Uint32Array([0x08070605, 0x04030201])); 7 | }); 8 | 9 | test('serialization | i2abLow', () => { 10 | expect( 11 | i2abLow(new Uint32Array([0x01020304, 0x05060708])), 12 | ).toEqual( 13 | new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]).buffer, 14 | ); 15 | }); 16 | 17 | test('serialization | i2abBig', () => { 18 | expect( 19 | i2abLow(new Uint32Array([0x01020304, 0x05060708])) instanceof ArrayBuffer, 20 | ).toBeTruthy(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/serialization/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | export function i2h(number: number) { 3 | return `00000000${number.toString(16)}`.slice(-8); 4 | } 5 | 6 | export const utf8decoder = self.TextDecoder ? new TextDecoder() : { 7 | decode: (buf: ArrayBuffer | SharedArrayBuffer) => { 8 | const uint = new Uint8Array(buf); 9 | let str = ''; 10 | 11 | for (let i = 0; i < uint.byteLength; i++) str += String.fromCharCode(uint[i]); 12 | 13 | return decodeURIComponent(escape(str)); 14 | }, 15 | }; 16 | 17 | export const utf8encoder = self.TextEncoder ? new TextEncoder() : { 18 | encode: (src: string) => { 19 | const str = unescape(encodeURIComponent(src)); 20 | const uint = new Uint8Array(str.length); 21 | 22 | for (let i = 0; i < str.length; i++) uint[i] = str.charCodeAt(i); 23 | 24 | return uint; 25 | }, 26 | }; 27 | 28 | /** 29 | * Randomize 30 | */ 31 | export function randomize(buf: Uint32Array | Uint8Array, start: number = 0) { 32 | if (!start && self.crypto && self.crypto.getRandomValues) { 33 | self.crypto.getRandomValues(buf); 34 | } else { 35 | const base = buf instanceof Uint32Array ? 0xFFFFFFFF : 0xFF; 36 | for (let i = start; i < buf.length; i += 1) { 37 | buf[i] = Math.ceil(Math.random() * base); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Randomize 44 | */ 45 | export function reverse32(buf: Uint32Array) { 46 | const reversed = new Uint32Array(buf.length); 47 | 48 | for (let i = 0; i < buf.length; i++) { 49 | reversed[i] = ( 50 | (buf[buf.length - i - 1] & 0xFF) << 24 51 | ^ ((buf[buf.length - i - 1] >> 8) & 0xFF) << 16 52 | ^ ((buf[buf.length - i - 1] >> 16) & 0xFF) << 8 53 | ^ ((buf[buf.length - i - 1] >>> 24) & 0xFF) 54 | ) >>> 0; 55 | } 56 | 57 | return reversed; 58 | } 59 | 60 | /** 61 | * Uint32Array -> ArrayBuffer (low-endian os) 62 | */ 63 | export function i2abLow(buf: Uint32Array): ArrayBuffer { 64 | const uint8 = new Uint8Array(buf.length * 4); 65 | let i = 0; 66 | 67 | for (let j = 0; j < buf.length; j++) { 68 | const int = buf[j]; 69 | 70 | uint8[i++] = int >>> 24; 71 | uint8[i++] = (int >> 16) & 0xFF; 72 | uint8[i++] = (int >> 8) & 0xFF; 73 | uint8[i++] = int & 0xFF; 74 | } 75 | 76 | return uint8.buffer; 77 | } 78 | 79 | /** 80 | * Uint32Array -> ArrayBuffer (big-endian os) 81 | */ 82 | export function i2abBig(buf: Uint32Array): ArrayBuffer { 83 | return buf.buffer; 84 | } 85 | 86 | /** 87 | * ArrayBuffer -> Uint32Array (low-endian os) 88 | */ 89 | export function ab2iLow(ab: ArrayBuffer | SharedArrayBuffer | Uint8Array): Uint32Array { 90 | const uint8 = new Uint8Array(ab); 91 | const buf = new Uint32Array(uint8.length / 4); 92 | 93 | for (let i = 0; i < uint8.length; i += 4) { 94 | buf[i / 4] = ( 95 | uint8[i] << 24 96 | ^ uint8[i + 1] << 16 97 | ^ uint8[i + 2] << 8 98 | ^ uint8[i + 3] 99 | ); 100 | } 101 | 102 | return buf; 103 | } 104 | 105 | /** 106 | * ArrayBuffer -> Uint32Array (big-endian os) 107 | */ 108 | export function ab2iBig(ab: ArrayBuffer | SharedArrayBuffer | Uint8Array): Uint32Array { 109 | return new Uint32Array(ab); 110 | } 111 | 112 | export const isBigEndian = new Uint8Array(new Uint32Array([0x01020304]))[0] === 0x01; 113 | export const i2ab = isBigEndian ? i2abBig : i2abLow; 114 | export const ab2i = isBigEndian ? ab2iBig : ab2iLow; 115 | -------------------------------------------------------------------------------- /src/serialization/writer.test.ts: -------------------------------------------------------------------------------- 1 | import Writer32 from './writer'; 2 | 3 | test('serialization | writer32 res_pq', () => { 4 | const writer = new Writer32(new Uint32Array(21)); 5 | 6 | writer.long('00'.repeat(8)); 7 | writer.long('51e57ac91e83c801'); 8 | 9 | expect(writer.buf).toEqual( 10 | new Uint32Array([ 11 | 0x00000000, 0x00000000, 0x01C8831E, 0xC97AE551, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 12 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 13 | ]), 14 | ); 15 | 16 | writer.int32(64); 17 | writer.int32(0x05162463); 18 | writer.int128(new Uint32Array([0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC])); 19 | writer.int128(new Uint32Array([0xA5CF4D33, 0xF4A11EA8, 0x77BA4AA5, 0x73907330])); 20 | writer.bytes(new Uint8Array([0x17, 0xED, 0x48, 0x94, 0x1A, 0x08, 0xF9, 0x81]).buffer); 21 | writer.int32(0x1cb5c415); 22 | writer.int32(1); 23 | writer.long('c3b42b026ce86b21'); 24 | 25 | expect(writer.buf).toEqual( 26 | new Uint32Array([ 27 | 0x00000000, 0x00000000, 0x01C8831E, 0xC97AE551, 0x40000000, 0x63241605, 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 0xA5CF4D33, 28 | 0xF4A11EA8, 0x77BA4AA5, 0x73907330, 0x0817ED48, 0x941A08F9, 0x81000000, 0x15C4B51C, 0x01000000, 0x216BE86C, 0x022BB4C3, 29 | ]), 30 | ); 31 | }); 32 | 33 | test('serialization | writer32 int256', () => { 34 | const writer = new Writer32(new Uint32Array(8)); 35 | 36 | const buf = new Uint32Array([ 37 | 0x3E054982, 0x8CCA27E9, 0x66B301A4, 0x8FECE2FC, 38 | 0xA5CF4D33, 0xF4A11EA8, 0x77BA4AA5, 0x73907330, 39 | ]); 40 | 41 | writer.int256(buf); 42 | 43 | expect(writer.buf).toEqual(buf); 44 | }); 45 | 46 | test('serialization | writer32 int64', () => { 47 | const writer = new Writer32(new Uint32Array(2)); 48 | 49 | const buf = new Uint32Array([ 50 | 0x3E054982, 0x8CCA27E9, 51 | ]); 52 | 53 | writer.int64('3E0549828CCA27E9'); 54 | 55 | expect(writer.buf).toEqual(buf); 56 | }); 57 | 58 | test('serialization | writer32 string', () => { 59 | const buffer = new Uint32Array([ 60 | 0xfe2c0100, 0x4c6f7265, 0x6d206970, 0x73756d20, 0x646f6c6f, 0x72207369, 0x7420616d, 0x65742c20, 0x636f6e73, 0x65637465, 0x74757220, 61 | 0x61646970, 0x69736369, 0x6e672065, 0x6c69742e, 0x20536564, 0x20636f6e, 0x67756520, 0x64696374, 0x756d2065, 0x6e696d20, 0x65676574, 62 | 0x20636f6e, 0x76616c6c, 0x69732e20, 0x51756973, 0x71756520, 0x696d7065, 0x72646965, 0x7420636f, 0x6e76616c, 0x6c697320, 0x72697375, 63 | 0x73206e65, 0x63207665, 0x6e656e61, 0x7469732e, 0x20536564, 0x20616c69, 0x71756574, 0x20706861, 0x72657472, 0x6120706f, 0x72747469, 64 | 0x746f722e, 0x20437572, 0x61626974, 0x75722065, 0x66666963, 0x69747572, 0x20696163, 0x756c6973, 0x20746f72, 0x746f7220, 0x6574206c, 65 | 0x6163696e, 0x69612e20, 0x41656e65, 0x616e2063, 0x6f6e7661, 0x6c6c6973, 0x20697073, 0x756d2063, 0x6f6d6d6f, 0x646f2065, 0x6c656d65, 66 | 0x6e74756d, 0x20706f72, 0x74612e20, 0x436c6173, 0x73206170, 0x74656e74, 0x20746163, 0x69746920, 0x706f7375, 0x6572652e, 0x40000000, 67 | ]); 68 | 69 | const writer = new Writer32(new Uint32Array(buffer.length)); 70 | 71 | writer.string( 72 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed congue dictum enim eget convallis. Quisque imperdiet convallis risus nec venenatis. ' 73 | + 'Sed aliquet pharetra porttitor. Curabitur efficitur iaculis tortor et lacinia. Aenean convallis ipsum commodo elementum porta. Class aptent ' 74 | + 'taciti posuere.', 75 | ); 76 | 77 | writer.int32(0x40); 78 | 79 | expect(writer.buf).toEqual(buffer); 80 | }); 81 | 82 | test('serialization | writer32 bool', () => { 83 | const buffer = new Uint32Array([ 84 | 0xb5757299, 0x379779bc, 85 | ]); 86 | 87 | const writer = new Writer32(new Uint32Array(buffer.length)); 88 | 89 | writer.bool(true); 90 | writer.bool(false); 91 | 92 | expect(writer.buf).toEqual(buffer); 93 | }); 94 | -------------------------------------------------------------------------------- /src/serialization/writer.ts: -------------------------------------------------------------------------------- 1 | import { utf8encoder } from './utils'; 2 | 3 | /** 4 | * Type Language Serialization 5 | */ 6 | export default class Writer32 { 7 | buf: Uint32Array; 8 | pos: number; 9 | 10 | constructor(buf: Uint32Array, start: number = 0) { 11 | this.buf = buf; 12 | this.pos = start; 13 | } 14 | 15 | int32(number: number) { 16 | this.buf[this.pos++] = ( 17 | (number >>> 24) 18 | ^ ((number >> 16) & 0xFF) << 8 19 | ^ ((number >> 8) & 0xFF) << 16 20 | ^ (number & 0xFF) << 24 21 | ) >>> 0; 22 | } 23 | 24 | int64(number: string) { 25 | this.buf[this.pos++] = +`0x${number.slice(0, 8)}`; 26 | this.buf[this.pos++] = +`0x${number.slice(8, 16)}`; 27 | } 28 | 29 | long(number: string) { 30 | this.int32(+`0x${number.slice(8, 16)}`); 31 | this.int32(+`0x${number.slice(0, 8)}`); 32 | } 33 | 34 | int128(src: Uint32Array) { 35 | for (let i = 0; i < 4; i++) this.buf[this.pos++] = src[i]; 36 | } 37 | 38 | int256(src: Uint32Array) { 39 | for (let i = 0; i < 8; i++) this.buf[this.pos++] = src[i]; 40 | } 41 | 42 | double(float: number) { 43 | const buf = new Uint32Array(new Float64Array([float])); 44 | for (let i = 0; i < 2; i++) this.buf[this.pos++] = buf[i]; 45 | } 46 | 47 | bytes(src: ArrayBuffer | SharedArrayBuffer | Uint8Array) { 48 | const buf = new Uint8Array(src); 49 | const len = buf.byteLength; 50 | let i = 0; 51 | 52 | if (len < 0xFE) { 53 | this.buf[this.pos++] = ( 54 | len << 24 55 | ^ buf[i++] << 16 56 | ^ buf[i++] << 8 57 | ^ buf[i++] 58 | ) >>> 0; 59 | } else { 60 | this.buf[this.pos++] = (0xFE000000 61 | ^ (len & 0xFF) << 16 62 | ^ ((len >> 8) & 0xFF) << 8 63 | ^ ((len >> 16) & 0xFF) 64 | ) >>> 0; 65 | } 66 | 67 | for (; i < len; i += 4) { 68 | this.buf[this.pos++] = ( 69 | buf[i] << 24 70 | ^ buf[i + 1] << 16 71 | ^ buf[i + 2] << 8 72 | ^ buf[i + 3] 73 | ) >>> 0; 74 | } 75 | } 76 | 77 | string(text: string) { 78 | return this.bytes(utf8encoder.encode(text)); 79 | } 80 | 81 | bool(flag: boolean) { 82 | return flag ? this.int32(0x997275b5) : this.int32(0xbc799737); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/tl/index.ts: -------------------------------------------------------------------------------- 1 | import { default as buildMtproto } from './mtproto/builder'; 2 | import { default as parseMtproto } from './mtproto/parser'; 3 | import { default as buildLayer } from './layer113/builder'; 4 | import { default as parseLayer } from './layer113/parser'; 5 | import { Reader32, Writer32 } from '../serialization'; 6 | import { MethodDeclMap as MtprotoMethodDeclMap } from './mtproto/types'; 7 | import { MethodDeclMap as LayerMethodDeclMap } from './layer113/types'; 8 | 9 | // types 10 | export { 11 | ResPQ, BadMsgNotification, NewSession, RpcResult, Req_DH_params, 12 | Set_client_DH_params, Server_DH_inner_data, 13 | } from './mtproto/types'; 14 | 15 | export { 16 | UpdateDeclMap, 17 | AuthBindTempAuthKey, 18 | InputCheckPasswordSRP, 19 | AccountPassword, 20 | } from './layer113/types'; 21 | 22 | export interface MethodDeclMap extends MtprotoMethodDeclMap, LayerMethodDeclMap { 23 | } 24 | 25 | export const parse = (reader: Reader32) => parseMtproto(reader, parseLayer); 26 | 27 | const sharedBuffer = new Uint32Array(128 * 1024); 28 | const writer = new Writer32(sharedBuffer); 29 | 30 | export const build = (o: any) => { 31 | writer.pos = 0; 32 | buildMtproto(writer, o, () => buildLayer(writer, o)); 33 | return writer.buf.subarray(0, writer.pos); 34 | }; 35 | -------------------------------------------------------------------------------- /src/tl/mtproto/builder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | /* eslint-disable quote-props */ 3 | /* eslint-disable spaced-comment */ 4 | /* eslint-disable max-len */ 5 | /* eslint-disable operator-linebreak */ 6 | /* eslint-disable semi-style */ 7 | 8 | /*******************************************************************************************/ 9 | /* This file was automatically generated (https://github.com/misupov/tg-schema-generator). */ 10 | /* */ 11 | /* Do not make changes to this file unless you know what you are doing -- modify */ 12 | /* the tool instead. */ 13 | /* */ 14 | /* Source: mtproto.json (md5: 1ef25a905cf20e6819483f8234f36b6b) */ 15 | /* Time: Thursday, 07 May 2020 06:31:44 (UTC) */ 16 | /* */ 17 | /*******************************************************************************************/ 18 | 19 | interface Writer { 20 | int32(value: number) : void; 21 | long(value: string): void; 22 | int128(value: Uint32Array): void; 23 | int256(value: Uint32Array): void; 24 | double(value: number): void; 25 | string(value: string): void; 26 | bytes(value: ArrayBuffer | SharedArrayBuffer | Uint8Array): void; 27 | } 28 | 29 | let w: Writer; 30 | let fallbackBuilder: ((stream: Writer, o: any) => void) | undefined; 31 | 32 | export default function build(writer: Writer, o: any, fallback?: (stream: Writer, o: any) => void) { 33 | w = writer; 34 | fallbackBuilder = fallback; 35 | return obj(o); 36 | } 37 | 38 | const _resPQ = (o: any) => { 39 | i128(o.nonce); 40 | i128(o.server_nonce); 41 | bytes(o.pq); 42 | vector(i64, o.server_public_key_fingerprints); 43 | }; 44 | 45 | const _p_q_inner_data = (o: any) => { 46 | bytes(o.pq); 47 | bytes(o.p); 48 | bytes(o.q); 49 | i128(o.nonce); 50 | i128(o.server_nonce); 51 | i256(o.new_nonce); 52 | }; 53 | 54 | const _p_q_inner_data_dc = (o: any) => { 55 | bytes(o.pq); 56 | bytes(o.p); 57 | bytes(o.q); 58 | i128(o.nonce); 59 | i128(o.server_nonce); 60 | i256(o.new_nonce); 61 | i32(o.dc); 62 | }; 63 | 64 | const _p_q_inner_data_temp = (o: any) => { 65 | bytes(o.pq); 66 | bytes(o.p); 67 | bytes(o.q); 68 | i128(o.nonce); 69 | i128(o.server_nonce); 70 | i256(o.new_nonce); 71 | i32(o.expires_in); 72 | }; 73 | 74 | const _p_q_inner_data_temp_dc = (o: any) => { 75 | bytes(o.pq); 76 | bytes(o.p); 77 | bytes(o.q); 78 | i128(o.nonce); 79 | i128(o.server_nonce); 80 | i256(o.new_nonce); 81 | i32(o.dc); 82 | i32(o.expires_in); 83 | }; 84 | 85 | const _server_DH_params_fail = (o: any) => { 86 | i128(o.nonce); 87 | i128(o.server_nonce); 88 | i128(o.new_nonce_hash); 89 | }; 90 | 91 | const _server_DH_params_ok = (o: any) => { 92 | i128(o.nonce); 93 | i128(o.server_nonce); 94 | bytes(o.encrypted_answer); 95 | }; 96 | 97 | const _server_DH_inner_data = (o: any) => { 98 | i128(o.nonce); 99 | i128(o.server_nonce); 100 | i32(o.g); 101 | bytes(o.dh_prime); 102 | bytes(o.g_a); 103 | i32(o.server_time); 104 | }; 105 | 106 | const _client_DH_inner_data = (o: any) => { 107 | i128(o.nonce); 108 | i128(o.server_nonce); 109 | i64(o.retry_id); 110 | bytes(o.g_b); 111 | }; 112 | 113 | const _dh_gen_ok = (o: any) => { 114 | i128(o.nonce); 115 | i128(o.server_nonce); 116 | i128(o.new_nonce_hash1); 117 | }; 118 | 119 | const _dh_gen_retry = (o: any) => { 120 | i128(o.nonce); 121 | i128(o.server_nonce); 122 | i128(o.new_nonce_hash2); 123 | }; 124 | 125 | const _dh_gen_fail = (o: any) => { 126 | i128(o.nonce); 127 | i128(o.server_nonce); 128 | i128(o.new_nonce_hash3); 129 | }; 130 | 131 | const _rpc_result = (o: any) => { 132 | i64(o.req_msg_id); 133 | obj(o.result); 134 | }; 135 | 136 | const _rpc_error = (o: any) => { 137 | i32(o.error_code); 138 | str(o.error_message); 139 | }; 140 | 141 | const _rpc_answer_dropped = (o: any) => { 142 | i64(o.msg_id); 143 | i32(o.seq_no); 144 | i32(o.bytes); 145 | }; 146 | 147 | const _future_salt = (o: any) => { 148 | i32(o.valid_since); 149 | i32(o.valid_until); 150 | i64(o.salt); 151 | }; 152 | 153 | const _future_salts = (o: any) => { 154 | i64(o.req_msg_id); 155 | i32(o.now); 156 | vector(_future_salt, o.salts); 157 | }; 158 | 159 | const _pong = (o: any) => { 160 | i64(o.msg_id); 161 | i64(o.ping_id); 162 | }; 163 | 164 | const _new_session_created = (o: any) => { 165 | i64(o.first_msg_id); 166 | i64(o.unique_id); 167 | i64(o.server_salt); 168 | }; 169 | 170 | const _msg_container = (o: any) => { 171 | vector(obj, o.messages); 172 | }; 173 | 174 | const _message = (o: any) => { 175 | i64(o.msg_id); 176 | i32(o.seqno); 177 | i32(o.bytes); 178 | obj(o.body); 179 | }; 180 | 181 | const _msg_copy = (o: any) => { 182 | obj(o.orig_message); 183 | }; 184 | 185 | const _gzip_packed = (o: any) => { 186 | bytes(o.packed_data); 187 | }; 188 | 189 | const _msgs_ack = (o: any) => { 190 | vector(i64, o.msg_ids); 191 | }; 192 | 193 | const _bad_msg_notification = (o: any) => { 194 | i64(o.bad_msg_id); 195 | i32(o.bad_msg_seqno); 196 | i32(o.error_code); 197 | }; 198 | 199 | const _bad_server_salt = (o: any) => { 200 | i64(o.bad_msg_id); 201 | i32(o.bad_msg_seqno); 202 | i32(o.error_code); 203 | i64(o.new_server_salt); 204 | }; 205 | 206 | const _msg_resend_req = (o: any) => { 207 | vector(i64, o.msg_ids); 208 | }; 209 | 210 | const _msg_resend_ans_req = (o: any) => { 211 | vector(i64, o.msg_ids); 212 | }; 213 | 214 | const _msgs_state_req = (o: any) => { 215 | vector(i64, o.msg_ids); 216 | }; 217 | 218 | const _msgs_state_info = (o: any) => { 219 | i64(o.req_msg_id); 220 | bytes(o.info); 221 | }; 222 | 223 | const _msgs_all_info = (o: any) => { 224 | vector(i64, o.msg_ids); 225 | bytes(o.info); 226 | }; 227 | 228 | const _msg_detailed_info = (o: any) => { 229 | i64(o.msg_id); 230 | i64(o.answer_msg_id); 231 | i32(o.bytes); 232 | i32(o.status); 233 | }; 234 | 235 | const _msg_new_detailed_info = (o: any) => { 236 | i64(o.answer_msg_id); 237 | i32(o.bytes); 238 | i32(o.status); 239 | }; 240 | 241 | const _bind_auth_key_inner = (o: any) => { 242 | i64(o.nonce); 243 | i64(o.temp_auth_key_id); 244 | i64(o.perm_auth_key_id); 245 | i64(o.temp_session_id); 246 | i32(o.expires_at); 247 | }; 248 | 249 | const _destroy_session_ok = (o: any) => { 250 | i64(o.session_id); 251 | }; 252 | 253 | const _destroy_session_none = (o: any) => { 254 | i64(o.session_id); 255 | }; 256 | 257 | const _req_pq = (o: any) => { 258 | i128(o.nonce); 259 | }; 260 | 261 | const _req_pq_multi = (o: any) => { 262 | i128(o.nonce); 263 | }; 264 | 265 | const _req_DH_params = (o: any) => { 266 | i128(o.nonce); 267 | i128(o.server_nonce); 268 | bytes(o.p); 269 | bytes(o.q); 270 | i64(o.public_key_fingerprint); 271 | bytes(o.encrypted_data); 272 | }; 273 | 274 | const _set_client_DH_params = (o: any) => { 275 | i128(o.nonce); 276 | i128(o.server_nonce); 277 | bytes(o.encrypted_data); 278 | }; 279 | 280 | const _rpc_drop_answer = (o: any) => { 281 | i64(o.req_msg_id); 282 | }; 283 | 284 | const _get_future_salts = (o: any) => { 285 | i32(o.num); 286 | }; 287 | 288 | const _ping = (o: any) => { 289 | i64(o.ping_id); 290 | }; 291 | 292 | const _ping_delay_disconnect = (o: any) => { 293 | i64(o.ping_id); 294 | i32(o.disconnect_delay); 295 | }; 296 | 297 | const _http_wait = (o: any) => { 298 | i32(o.max_delay); 299 | i32(o.wait_after); 300 | i32(o.max_wait); 301 | }; 302 | 303 | const _destroy_session = (o: any) => { 304 | i64(o.session_id); 305 | }; 306 | 307 | 308 | const builderMap: Record void)?]> = { 309 | 'vector': [0x1cb5c415], 310 | 'resPQ': [0x5162463, _resPQ], 311 | 'p_q_inner_data': [0x83c95aec, _p_q_inner_data], 312 | 'p_q_inner_data_dc': [0xa9f55f95, _p_q_inner_data_dc], 313 | 'p_q_inner_data_temp': [0x3c6a84d4, _p_q_inner_data_temp], 314 | 'p_q_inner_data_temp_dc': [0x56fddf88, _p_q_inner_data_temp_dc], 315 | 'server_DH_params_fail': [0x79cb045d, _server_DH_params_fail], 316 | 'server_DH_params_ok': [0xd0e8075c, _server_DH_params_ok], 317 | 'server_DH_inner_data': [0xb5890dba, _server_DH_inner_data], 318 | 'client_DH_inner_data': [0x6643b654, _client_DH_inner_data], 319 | 'dh_gen_ok': [0x3bcbf734, _dh_gen_ok], 320 | 'dh_gen_retry': [0x46dc1fb9, _dh_gen_retry], 321 | 'dh_gen_fail': [0xa69dae02, _dh_gen_fail], 322 | 'rpc_result': [0xf35c6d01, _rpc_result], 323 | 'rpc_error': [0x2144ca19, _rpc_error], 324 | 'rpc_answer_unknown': [0x5e2ad36e], 325 | 'rpc_answer_dropped_running': [0xcd78e586], 326 | 'rpc_answer_dropped': [0xa43ad8b7, _rpc_answer_dropped], 327 | 'future_salt': [0x949d9dc, _future_salt], 328 | 'future_salts': [0xae500895, _future_salts], 329 | 'pong': [0x347773c5, _pong], 330 | 'new_session_created': [0x9ec20908, _new_session_created], 331 | 'msg_container': [0x73f1f8dc, _msg_container], 332 | 'message': [0x5bb8e511, _message], 333 | 'msg_copy': [0xe06046b2, _msg_copy], 334 | 'gzip_packed': [0x3072cfa1, _gzip_packed], 335 | 'msgs_ack': [0x62d6b459, _msgs_ack], 336 | 'bad_msg_notification': [0xa7eff811, _bad_msg_notification], 337 | 'bad_server_salt': [0xedab447b, _bad_server_salt], 338 | 'msg_resend_req': [0x7d861a08, _msg_resend_req], 339 | 'msg_resend_ans_req': [0x8610baeb, _msg_resend_ans_req], 340 | 'msgs_state_req': [0xda69fb52, _msgs_state_req], 341 | 'msgs_state_info': [0x4deb57d, _msgs_state_info], 342 | 'msgs_all_info': [0x8cc0d131, _msgs_all_info], 343 | 'msg_detailed_info': [0x276d3ec6, _msg_detailed_info], 344 | 'msg_new_detailed_info': [0x809db6df, _msg_new_detailed_info], 345 | 'bind_auth_key_inner': [0x75a3f765, _bind_auth_key_inner], 346 | 'destroy_auth_key_ok': [0xf660e1d4], 347 | 'destroy_auth_key_none': [0xa9f2259], 348 | 'destroy_auth_key_fail': [0xea109b13], 349 | 'destroy_session_ok': [0xe22045fc, _destroy_session_ok], 350 | 'destroy_session_none': [0x62d350c9, _destroy_session_none], 351 | 'req_pq': [0x60469778, _req_pq], 352 | 'req_pq_multi': [0xbe7e8ef1, _req_pq_multi], 353 | 'req_DH_params': [0xd712e4be, _req_DH_params], 354 | 'set_client_DH_params': [0xf5045f1f, _set_client_DH_params], 355 | 'rpc_drop_answer': [0x58e4a740, _rpc_drop_answer], 356 | 'get_future_salts': [0xb921bd04, _get_future_salts], 357 | 'ping': [0x7abe77ec, _ping], 358 | 'ping_delay_disconnect': [0xf3427b8c, _ping_delay_disconnect], 359 | 'http_wait': [0x9299359f, _http_wait], 360 | 'destroy_auth_key': [0xd1435160], 361 | 'destroy_session': [0xe7512126, _destroy_session], 362 | }; 363 | 364 | const i32 = (value: number) => w.int32(value); 365 | const i64 = (value: string) => w.long(value); 366 | const i128 = (value: Uint32Array) => w.int128(value); 367 | const i256 = (value: Uint32Array) => w.int256(value); 368 | const str = (value: string) => w.string(value); 369 | const bytes = (value: ArrayBuffer) => w.bytes(value); 370 | 371 | const vector = (fn: (value: any) => void, value: Array) => { 372 | i32(0x1cb5c415); 373 | i32(value.length); 374 | for (let i = 0; i < value.length; i++) { 375 | fn(value[i]); 376 | } 377 | }; 378 | 379 | const obj = (o: any, bare = false) => { 380 | const descriptor = builderMap[o._]; 381 | if (descriptor) { 382 | const [id, fn] = descriptor; 383 | if (!bare) i32(id); 384 | if (fn) fn(o); 385 | } else if (fallbackBuilder) { 386 | fallbackBuilder(w, o); 387 | } else { 388 | console.error(`Cannot serialize object ${JSON.stringify(o)}`); 389 | } 390 | }; 391 | -------------------------------------------------------------------------------- /src/tl/mtproto/parser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | /* eslint-disable quote-props */ 3 | /* eslint-disable spaced-comment */ 4 | /* eslint-disable max-len */ 5 | /* eslint-disable operator-linebreak */ 6 | /* eslint-disable semi-style */ 7 | 8 | /*******************************************************************************************/ 9 | /* This file was automatically generated (https://github.com/misupov/tg-schema-generator). */ 10 | /* */ 11 | /* Do not make changes to this file unless you know what you are doing -- modify */ 12 | /* the tool instead. */ 13 | /* */ 14 | /* Source: mtproto.json (md5: 1ef25a905cf20e6819483f8234f36b6b) */ 15 | /* Time: Sunday, 07 June 2020 15:17:31 (UTC) */ 16 | /* */ 17 | /*******************************************************************************************/ 18 | 19 | interface Reader { 20 | int32(): number; 21 | long(): string; 22 | int128(): Uint32Array; 23 | int256(): Uint32Array; 24 | double(): number; 25 | string(): string; 26 | bytes(): ArrayBuffer; 27 | rollback(): void; 28 | } 29 | 30 | let r: Reader; 31 | let fallbackParse: ((stream: Reader) => any) | undefined; 32 | 33 | export default function parse(reader: Reader, fallback?: (stream: Reader) => any) { 34 | r = reader; 35 | fallbackParse = fallback; 36 | return obj(); 37 | } 38 | 39 | const _vector = () => vector(obj, true); 40 | const _resPQ: any = () => ({ _: 'resPQ', nonce: i128(), server_nonce: i128(), pq: bytes(), server_public_key_fingerprints: vector(i64) }); 41 | const _p_q_inner_data: any = () => ({ _: 'p_q_inner_data', pq: bytes(), p: bytes(), q: bytes(), nonce: i128(), server_nonce: i128(), new_nonce: i256() }); 42 | const _p_q_inner_data_dc: any = () => ({ _: 'p_q_inner_data_dc', pq: bytes(), p: bytes(), q: bytes(), nonce: i128(), server_nonce: i128(), new_nonce: i256(), dc: i32() }); 43 | const _p_q_inner_data_temp: any = () => ({ _: 'p_q_inner_data_temp', pq: bytes(), p: bytes(), q: bytes(), nonce: i128(), server_nonce: i128(), new_nonce: i256(), expires_in: i32() }); 44 | const _p_q_inner_data_temp_dc: any = () => ({ _: 'p_q_inner_data_temp_dc', pq: bytes(), p: bytes(), q: bytes(), nonce: i128(), server_nonce: i128(), new_nonce: i256(), dc: i32(), expires_in: i32() }); 45 | const _server_DH_params_fail: any = () => ({ _: 'server_DH_params_fail', nonce: i128(), server_nonce: i128(), new_nonce_hash: i128() }); 46 | const _server_DH_params_ok: any = () => ({ _: 'server_DH_params_ok', nonce: i128(), server_nonce: i128(), encrypted_answer: bytes() }); 47 | const _server_DH_inner_data: any = () => ({ _: 'server_DH_inner_data', nonce: i128(), server_nonce: i128(), g: i32(), dh_prime: bytes(), g_a: bytes(), server_time: i32() }); 48 | const _client_DH_inner_data: any = () => ({ _: 'client_DH_inner_data', nonce: i128(), server_nonce: i128(), retry_id: i64(), g_b: bytes() }); 49 | const _dh_gen_ok: any = () => ({ _: 'dh_gen_ok', nonce: i128(), server_nonce: i128(), new_nonce_hash1: i128() }); 50 | const _dh_gen_retry: any = () => ({ _: 'dh_gen_retry', nonce: i128(), server_nonce: i128(), new_nonce_hash2: i128() }); 51 | const _dh_gen_fail: any = () => ({ _: 'dh_gen_fail', nonce: i128(), server_nonce: i128(), new_nonce_hash3: i128() }); 52 | const _rpc_result: any = () => ({ _: 'rpc_result', req_msg_id: i64(), result: obj() }); 53 | const _rpc_error: any = () => ({ _: 'rpc_error', error_code: i32(), error_message: str() }); 54 | const _rpc_answer_unknown: any = () => ({ _: 'rpc_answer_unknown' }); 55 | const _rpc_answer_dropped_running: any = () => ({ _: 'rpc_answer_dropped_running' }); 56 | const _rpc_answer_dropped: any = () => ({ _: 'rpc_answer_dropped', msg_id: i64(), seq_no: i32(), bytes: i32() }); 57 | const _future_salt: any = () => ({ _: 'future_salt', valid_since: i32(), valid_until: i32(), salt: i64() }); 58 | const _future_salts: any = () => ({ _: 'future_salts', req_msg_id: i64(), now: i32(), salts: vector(_future_salt, true) }); 59 | const _pong: any = () => ({ _: 'pong', msg_id: i64(), ping_id: i64() }); 60 | const _new_session_created: any = () => ({ _: 'new_session_created', first_msg_id: i64(), unique_id: i64(), server_salt: i64() }); 61 | const _msg_container: any = () => ({ _: 'msg_container', messages: vector(_message, true) }); 62 | const _message: any = () => ({ _: 'message', msg_id: i64(), seqno: i32(), bytes: i32(), body: obj() }); 63 | const _msg_copy: any = () => ({ _: 'msg_copy', orig_message: obj() }); 64 | const _gzip_packed: any = () => ({ _: 'gzip_packed', packed_data: bytes() }); 65 | const _msgs_ack: any = () => ({ _: 'msgs_ack', msg_ids: vector(i64) }); 66 | const _bad_msg_notification: any = () => ({ _: 'bad_msg_notification', bad_msg_id: i64(), bad_msg_seqno: i32(), error_code: i32() }); 67 | const _bad_server_salt: any = () => ({ _: 'bad_server_salt', bad_msg_id: i64(), bad_msg_seqno: i32(), error_code: i32(), new_server_salt: i64() }); 68 | const _msg_resend_req: any = () => ({ _: 'msg_resend_req', msg_ids: vector(i64) }); 69 | const _msg_resend_ans_req: any = () => ({ _: 'msg_resend_ans_req', msg_ids: vector(i64) }); 70 | const _msgs_state_req: any = () => ({ _: 'msgs_state_req', msg_ids: vector(i64) }); 71 | const _msgs_state_info: any = () => ({ _: 'msgs_state_info', req_msg_id: i64(), info: bytes() }); 72 | const _msgs_all_info: any = () => ({ _: 'msgs_all_info', msg_ids: vector(i64), info: bytes() }); 73 | const _msg_detailed_info: any = () => ({ _: 'msg_detailed_info', msg_id: i64(), answer_msg_id: i64(), bytes: i32(), status: i32() }); 74 | const _msg_new_detailed_info: any = () => ({ _: 'msg_new_detailed_info', answer_msg_id: i64(), bytes: i32(), status: i32() }); 75 | const _bind_auth_key_inner: any = () => ({ _: 'bind_auth_key_inner', nonce: i64(), temp_auth_key_id: i64(), perm_auth_key_id: i64(), temp_session_id: i64(), expires_at: i32() }); 76 | const _destroy_auth_key_ok: any = () => ({ _: 'destroy_auth_key_ok' }); 77 | const _destroy_auth_key_none: any = () => ({ _: 'destroy_auth_key_none' }); 78 | const _destroy_auth_key_fail: any = () => ({ _: 'destroy_auth_key_fail' }); 79 | const _destroy_session_ok: any = () => ({ _: 'destroy_session_ok', session_id: i64() }); 80 | const _destroy_session_none: any = () => ({ _: 'destroy_session_none', session_id: i64() }); 81 | 82 | const parserMap = new Map any>([ 83 | [0x1cb5c415, _vector], 84 | [0x5162463, _resPQ], 85 | [0x83c95aec, _p_q_inner_data], 86 | [0xa9f55f95, _p_q_inner_data_dc], 87 | [0x3c6a84d4, _p_q_inner_data_temp], 88 | [0x56fddf88, _p_q_inner_data_temp_dc], 89 | [0x79cb045d, _server_DH_params_fail], 90 | [0xd0e8075c, _server_DH_params_ok], 91 | [0xb5890dba, _server_DH_inner_data], 92 | [0x6643b654, _client_DH_inner_data], 93 | [0x3bcbf734, _dh_gen_ok], 94 | [0x46dc1fb9, _dh_gen_retry], 95 | [0xa69dae02, _dh_gen_fail], 96 | [0xf35c6d01, _rpc_result], 97 | [0x2144ca19, _rpc_error], 98 | [0x5e2ad36e, _rpc_answer_unknown], 99 | [0xcd78e586, _rpc_answer_dropped_running], 100 | [0xa43ad8b7, _rpc_answer_dropped], 101 | [0x949d9dc, _future_salt], 102 | [0xae500895, _future_salts], 103 | [0x347773c5, _pong], 104 | [0x9ec20908, _new_session_created], 105 | [0x73f1f8dc, _msg_container], 106 | [0x5bb8e511, _message], 107 | [0xe06046b2, _msg_copy], 108 | [0x3072cfa1, _gzip_packed], 109 | [0x62d6b459, _msgs_ack], 110 | [0xa7eff811, _bad_msg_notification], 111 | [0xedab447b, _bad_server_salt], 112 | [0x7d861a08, _msg_resend_req], 113 | [0x8610baeb, _msg_resend_ans_req], 114 | [0xda69fb52, _msgs_state_req], 115 | [0x4deb57d, _msgs_state_info], 116 | [0x8cc0d131, _msgs_all_info], 117 | [0x276d3ec6, _msg_detailed_info], 118 | [0x809db6df, _msg_new_detailed_info], 119 | [0x75a3f765, _bind_auth_key_inner], 120 | [0xf660e1d4, _destroy_auth_key_ok], 121 | [0xa9f2259, _destroy_auth_key_none], 122 | [0xea109b13, _destroy_auth_key_fail], 123 | [0xe22045fc, _destroy_session_ok], 124 | [0x62d350c9, _destroy_session_none], 125 | ]); 126 | 127 | const i32 = () => r.int32(); 128 | const i64 = () => r.long(); 129 | const i128 = () => r.int128(); 130 | const i256 = () => r.int256(); 131 | const str = () => r.string(); 132 | const bytes = () => r.bytes(); 133 | 134 | function vector(t: () => any, bare = false) { 135 | if (!bare) { i32(); /* ignoring constructor id. */ } 136 | const len = i32(); 137 | const result = []; 138 | for (let i = 0; i < len; ++i) result.push(t()); 139 | return result; 140 | } 141 | 142 | function obj() { 143 | const c = i32() >>> 0; 144 | const f = parserMap.get(c); 145 | if (f) return f(); 146 | if (fallbackParse) { 147 | r.rollback(); 148 | return fallbackParse(r); 149 | } 150 | console.error(`Unknown constructor 0x${c.toString(16)}.`); 151 | return undefined; 152 | } 153 | -------------------------------------------------------------------------------- /src/tl/mtproto/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | /* eslint-disable quote-props */ 3 | /* eslint-disable spaced-comment */ 4 | /* eslint-disable max-len */ 5 | /* eslint-disable operator-linebreak */ 6 | /* eslint-disable semi-style */ 7 | 8 | /*******************************************************************************************/ 9 | /* This file was automatically generated (https://github.com/misupov/tg-schema-generator). */ 10 | /* */ 11 | /* Do not make changes to this file unless you know what you are doing -- modify */ 12 | /* the tool instead. */ 13 | /* */ 14 | /* Source: mtproto.json (md5: 1ef25a905cf20e6819483f8234f36b6b) */ 15 | /* Time: Thursday, 07 May 2020 06:31:44 (UTC) */ 16 | /* */ 17 | /*******************************************************************************************/ 18 | 19 | /* CONSTRUCTORS */ 20 | 21 | export type ResPQ = 22 | | ResPQ.resPQ 23 | ; 24 | 25 | export namespace ResPQ { 26 | export type resPQ = { 27 | _: 'resPQ', 28 | nonce: Uint32Array, 29 | server_nonce: Uint32Array, 30 | pq: ArrayBuffer, 31 | server_public_key_fingerprints: string[], 32 | }; 33 | } 34 | 35 | export type P_Q_inner_data = 36 | | P_Q_inner_data.p_q_inner_data 37 | | P_Q_inner_data.p_q_inner_data_dc 38 | | P_Q_inner_data.p_q_inner_data_temp 39 | ; 40 | 41 | export namespace P_Q_inner_data { 42 | export type p_q_inner_data = { 43 | _: 'p_q_inner_data', 44 | pq: ArrayBuffer, 45 | p: ArrayBuffer, 46 | q: ArrayBuffer, 47 | nonce: Uint32Array, 48 | server_nonce: Uint32Array, 49 | new_nonce: Uint32Array, 50 | }; 51 | export type p_q_inner_data_dc = { 52 | _: 'p_q_inner_data_dc', 53 | pq: ArrayBuffer, 54 | p: ArrayBuffer, 55 | q: ArrayBuffer, 56 | nonce: Uint32Array, 57 | server_nonce: Uint32Array, 58 | new_nonce: Uint32Array, 59 | dc: number, 60 | }; 61 | export type p_q_inner_data_temp = { 62 | _: 'p_q_inner_data_temp', 63 | pq: ArrayBuffer, 64 | p: ArrayBuffer, 65 | q: ArrayBuffer, 66 | nonce: Uint32Array, 67 | server_nonce: Uint32Array, 68 | new_nonce: Uint32Array, 69 | expires_in: number, 70 | }; 71 | } 72 | 73 | export type P_Q_inner_d = 74 | | P_Q_inner_d.p_q_inner_data_temp_dc 75 | ; 76 | 77 | export namespace P_Q_inner_d { 78 | export type p_q_inner_data_temp_dc = { 79 | _: 'p_q_inner_data_temp_dc', 80 | pq: ArrayBuffer, 81 | p: ArrayBuffer, 82 | q: ArrayBuffer, 83 | nonce: Uint32Array, 84 | server_nonce: Uint32Array, 85 | new_nonce: Uint32Array, 86 | dc: number, 87 | expires_in: number, 88 | }; 89 | } 90 | 91 | export type Server_DH_Params = 92 | | Server_DH_Params.server_DH_params_fail 93 | | Server_DH_Params.server_DH_params_ok 94 | ; 95 | 96 | export namespace Server_DH_Params { 97 | export type server_DH_params_fail = { 98 | _: 'server_DH_params_fail', 99 | nonce: Uint32Array, 100 | server_nonce: Uint32Array, 101 | new_nonce_hash: Uint32Array, 102 | }; 103 | export type server_DH_params_ok = { 104 | _: 'server_DH_params_ok', 105 | nonce: Uint32Array, 106 | server_nonce: Uint32Array, 107 | encrypted_answer: ArrayBuffer, 108 | }; 109 | } 110 | 111 | export type Server_DH_inner_data = 112 | | Server_DH_inner_data.server_DH_inner_data 113 | ; 114 | 115 | export namespace Server_DH_inner_data { 116 | export type server_DH_inner_data = { 117 | _: 'server_DH_inner_data', 118 | nonce: Uint32Array, 119 | server_nonce: Uint32Array, 120 | g: number, 121 | dh_prime: ArrayBuffer, 122 | g_a: ArrayBuffer, 123 | server_time: number, 124 | }; 125 | } 126 | 127 | export type Client_DH_Inner_Data = 128 | | Client_DH_Inner_Data.client_DH_inner_data 129 | ; 130 | 131 | export namespace Client_DH_Inner_Data { 132 | export type client_DH_inner_data = { 133 | _: 'client_DH_inner_data', 134 | nonce: Uint32Array, 135 | server_nonce: Uint32Array, 136 | retry_id: string, 137 | g_b: ArrayBuffer, 138 | }; 139 | } 140 | 141 | export type Set_client_DH_params_answer = 142 | | Set_client_DH_params_answer.dh_gen_ok 143 | | Set_client_DH_params_answer.dh_gen_retry 144 | | Set_client_DH_params_answer.dh_gen_fail 145 | ; 146 | 147 | export namespace Set_client_DH_params_answer { 148 | export type dh_gen_ok = { 149 | _: 'dh_gen_ok', 150 | nonce: Uint32Array, 151 | server_nonce: Uint32Array, 152 | new_nonce_hash1: Uint32Array, 153 | }; 154 | export type dh_gen_retry = { 155 | _: 'dh_gen_retry', 156 | nonce: Uint32Array, 157 | server_nonce: Uint32Array, 158 | new_nonce_hash2: Uint32Array, 159 | }; 160 | export type dh_gen_fail = { 161 | _: 'dh_gen_fail', 162 | nonce: Uint32Array, 163 | server_nonce: Uint32Array, 164 | new_nonce_hash3: Uint32Array, 165 | }; 166 | } 167 | 168 | export type RpcResult = 169 | | RpcResult.rpc_result 170 | ; 171 | 172 | export namespace RpcResult { 173 | export type rpc_result = { 174 | _: 'rpc_result', 175 | req_msg_id: string, 176 | result: any, 177 | }; 178 | } 179 | 180 | export type RpcError = 181 | | RpcError.rpc_error 182 | ; 183 | 184 | export namespace RpcError { 185 | export type rpc_error = { 186 | _: 'rpc_error', 187 | error_code: number, 188 | error_message: string, 189 | }; 190 | } 191 | 192 | export type RpcDropAnswer = 193 | | RpcDropAnswer.rpc_answer_unknown 194 | | RpcDropAnswer.rpc_answer_dropped_running 195 | | RpcDropAnswer.rpc_answer_dropped 196 | ; 197 | 198 | export namespace RpcDropAnswer { 199 | export type rpc_answer_unknown = { 200 | _: 'rpc_answer_unknown', 201 | }; 202 | export type rpc_answer_dropped_running = { 203 | _: 'rpc_answer_dropped_running', 204 | }; 205 | export type rpc_answer_dropped = { 206 | _: 'rpc_answer_dropped', 207 | msg_id: string, 208 | seq_no: number, 209 | bytes: number, 210 | }; 211 | } 212 | 213 | export type FutureSalt = 214 | | FutureSalt.future_salt 215 | ; 216 | 217 | export namespace FutureSalt { 218 | export type future_salt = { 219 | _: 'future_salt', 220 | valid_since: number, 221 | valid_until: number, 222 | salt: string, 223 | }; 224 | } 225 | 226 | export type FutureSalts = 227 | | FutureSalts.future_salts 228 | ; 229 | 230 | export namespace FutureSalts { 231 | export type future_salts = { 232 | _: 'future_salts', 233 | req_msg_id: string, 234 | now: number, 235 | salts: FutureSalt.future_salt[], 236 | }; 237 | } 238 | 239 | export type Pong = 240 | | Pong.pong 241 | ; 242 | 243 | export namespace Pong { 244 | export type pong = { 245 | _: 'pong', 246 | msg_id: string, 247 | ping_id: string, 248 | }; 249 | } 250 | 251 | export type NewSession = 252 | | NewSession.new_session_created 253 | ; 254 | 255 | export namespace NewSession { 256 | export type new_session_created = { 257 | _: 'new_session_created', 258 | first_msg_id: string, 259 | unique_id: string, 260 | server_salt: string, 261 | }; 262 | } 263 | 264 | export type MessageContainer = 265 | | MessageContainer.msg_container 266 | ; 267 | 268 | export namespace MessageContainer { 269 | export type msg_container = { 270 | _: 'msg_container', 271 | messages: Message[], 272 | }; 273 | } 274 | 275 | export type Message = 276 | | Message.message 277 | ; 278 | 279 | export namespace Message { 280 | export type message = { 281 | _: 'message', 282 | msg_id: string, 283 | seqno: number, 284 | bytes: number, 285 | body: any, 286 | }; 287 | } 288 | 289 | export type MessageCopy = 290 | | MessageCopy.msg_copy 291 | ; 292 | 293 | export namespace MessageCopy { 294 | export type msg_copy = { 295 | _: 'msg_copy', 296 | orig_message: Message, 297 | }; 298 | } 299 | 300 | export type Object = 301 | | Object.gzip_packed 302 | ; 303 | 304 | export namespace Object { 305 | export type gzip_packed = { 306 | _: 'gzip_packed', 307 | packed_data: ArrayBuffer, 308 | }; 309 | } 310 | 311 | export type MsgsAck = 312 | | MsgsAck.msgs_ack 313 | ; 314 | 315 | export namespace MsgsAck { 316 | export type msgs_ack = { 317 | _: 'msgs_ack', 318 | msg_ids: string[], 319 | }; 320 | } 321 | 322 | export type BadMsgNotification = 323 | | BadMsgNotification.bad_msg_notification 324 | | BadMsgNotification.bad_server_salt 325 | ; 326 | 327 | export namespace BadMsgNotification { 328 | export type bad_msg_notification = { 329 | _: 'bad_msg_notification', 330 | bad_msg_id: string, 331 | bad_msg_seqno: number, 332 | error_code: number, 333 | }; 334 | export type bad_server_salt = { 335 | _: 'bad_server_salt', 336 | bad_msg_id: string, 337 | bad_msg_seqno: number, 338 | error_code: number, 339 | new_server_salt: string, 340 | }; 341 | } 342 | 343 | export type MsgResendReq = 344 | | MsgResendReq.msg_resend_req 345 | | MsgResendReq.msg_resend_ans_req 346 | ; 347 | 348 | export namespace MsgResendReq { 349 | export type msg_resend_req = { 350 | _: 'msg_resend_req', 351 | msg_ids: string[], 352 | }; 353 | export type msg_resend_ans_req = { 354 | _: 'msg_resend_ans_req', 355 | msg_ids: string[], 356 | }; 357 | } 358 | 359 | export type MsgsStateReq = 360 | | MsgsStateReq.msgs_state_req 361 | ; 362 | 363 | export namespace MsgsStateReq { 364 | export type msgs_state_req = { 365 | _: 'msgs_state_req', 366 | msg_ids: string[], 367 | }; 368 | } 369 | 370 | export type MsgsStateInfo = 371 | | MsgsStateInfo.msgs_state_info 372 | ; 373 | 374 | export namespace MsgsStateInfo { 375 | export type msgs_state_info = { 376 | _: 'msgs_state_info', 377 | req_msg_id: string, 378 | info: ArrayBuffer, 379 | }; 380 | } 381 | 382 | export type MsgsAllInfo = 383 | | MsgsAllInfo.msgs_all_info 384 | ; 385 | 386 | export namespace MsgsAllInfo { 387 | export type msgs_all_info = { 388 | _: 'msgs_all_info', 389 | msg_ids: string[], 390 | info: ArrayBuffer, 391 | }; 392 | } 393 | 394 | export type MsgDetailedInfo = 395 | | MsgDetailedInfo.msg_detailed_info 396 | | MsgDetailedInfo.msg_new_detailed_info 397 | ; 398 | 399 | export namespace MsgDetailedInfo { 400 | export type msg_detailed_info = { 401 | _: 'msg_detailed_info', 402 | msg_id: string, 403 | answer_msg_id: string, 404 | bytes: number, 405 | status: number, 406 | }; 407 | export type msg_new_detailed_info = { 408 | _: 'msg_new_detailed_info', 409 | answer_msg_id: string, 410 | bytes: number, 411 | status: number, 412 | }; 413 | } 414 | 415 | export type BindAuthKeyInner = 416 | | BindAuthKeyInner.bind_auth_key_inner 417 | ; 418 | 419 | export namespace BindAuthKeyInner { 420 | export type bind_auth_key_inner = { 421 | _: 'bind_auth_key_inner', 422 | nonce: string, 423 | temp_auth_key_id: string, 424 | perm_auth_key_id: string, 425 | temp_session_id: string, 426 | expires_at: number, 427 | }; 428 | } 429 | 430 | export type DestroyAuthKeyRes = 431 | | DestroyAuthKeyRes.destroy_auth_key_ok 432 | | DestroyAuthKeyRes.destroy_auth_key_none 433 | | DestroyAuthKeyRes.destroy_auth_key_fail 434 | ; 435 | 436 | export namespace DestroyAuthKeyRes { 437 | export type destroy_auth_key_ok = { 438 | _: 'destroy_auth_key_ok', 439 | }; 440 | export type destroy_auth_key_none = { 441 | _: 'destroy_auth_key_none', 442 | }; 443 | export type destroy_auth_key_fail = { 444 | _: 'destroy_auth_key_fail', 445 | }; 446 | } 447 | 448 | export type DestroySessionRes = 449 | | DestroySessionRes.destroy_session_ok 450 | | DestroySessionRes.destroy_session_none 451 | ; 452 | 453 | export namespace DestroySessionRes { 454 | export type destroy_session_ok = { 455 | _: 'destroy_session_ok', 456 | session_id: string, 457 | }; 458 | export type destroy_session_none = { 459 | _: 'destroy_session_none', 460 | session_id: string, 461 | }; 462 | } 463 | 464 | export interface ConstructorDeclMap { 465 | 'resPQ': ResPQ.resPQ, 466 | 'p_q_inner_data': P_Q_inner_data.p_q_inner_data, 467 | 'p_q_inner_data_dc': P_Q_inner_data.p_q_inner_data_dc, 468 | 'p_q_inner_data_temp': P_Q_inner_data.p_q_inner_data_temp, 469 | 'p_q_inner_data_temp_dc': P_Q_inner_d.p_q_inner_data_temp_dc, 470 | 'server_DH_params_fail': Server_DH_Params.server_DH_params_fail, 471 | 'server_DH_params_ok': Server_DH_Params.server_DH_params_ok, 472 | 'server_DH_inner_data': Server_DH_inner_data.server_DH_inner_data, 473 | 'client_DH_inner_data': Client_DH_Inner_Data.client_DH_inner_data, 474 | 'dh_gen_ok': Set_client_DH_params_answer.dh_gen_ok, 475 | 'dh_gen_retry': Set_client_DH_params_answer.dh_gen_retry, 476 | 'dh_gen_fail': Set_client_DH_params_answer.dh_gen_fail, 477 | 'rpc_result': RpcResult.rpc_result, 478 | 'rpc_error': RpcError.rpc_error, 479 | 'rpc_answer_unknown': RpcDropAnswer.rpc_answer_unknown, 480 | 'rpc_answer_dropped_running': RpcDropAnswer.rpc_answer_dropped_running, 481 | 'rpc_answer_dropped': RpcDropAnswer.rpc_answer_dropped, 482 | 'future_salt': FutureSalt.future_salt, 483 | 'future_salts': FutureSalts.future_salts, 484 | 'pong': Pong.pong, 485 | 'new_session_created': NewSession.new_session_created, 486 | 'msg_container': MessageContainer.msg_container, 487 | 'message': Message.message, 488 | 'msg_copy': MessageCopy.msg_copy, 489 | 'gzip_packed': Object.gzip_packed, 490 | 'msgs_ack': MsgsAck.msgs_ack, 491 | 'bad_msg_notification': BadMsgNotification.bad_msg_notification, 492 | 'bad_server_salt': BadMsgNotification.bad_server_salt, 493 | 'msg_resend_req': MsgResendReq.msg_resend_req, 494 | 'msg_resend_ans_req': MsgResendReq.msg_resend_ans_req, 495 | 'msgs_state_req': MsgsStateReq.msgs_state_req, 496 | 'msgs_state_info': MsgsStateInfo.msgs_state_info, 497 | 'msgs_all_info': MsgsAllInfo.msgs_all_info, 498 | 'msg_detailed_info': MsgDetailedInfo.msg_detailed_info, 499 | 'msg_new_detailed_info': MsgDetailedInfo.msg_new_detailed_info, 500 | 'bind_auth_key_inner': BindAuthKeyInner.bind_auth_key_inner, 501 | 'destroy_auth_key_ok': DestroyAuthKeyRes.destroy_auth_key_ok, 502 | 'destroy_auth_key_none': DestroyAuthKeyRes.destroy_auth_key_none, 503 | 'destroy_auth_key_fail': DestroyAuthKeyRes.destroy_auth_key_fail, 504 | 'destroy_session_ok': DestroySessionRes.destroy_session_ok, 505 | 'destroy_session_none': DestroySessionRes.destroy_session_none, 506 | } 507 | 508 | /* METHODS */ 509 | 510 | export type Req_pq = { 511 | nonce: Uint32Array, 512 | }; 513 | 514 | export type Req_pq_multi = { 515 | nonce: Uint32Array, 516 | }; 517 | 518 | export type Req_DH_params = { 519 | nonce: Uint32Array, 520 | server_nonce: Uint32Array, 521 | p: ArrayBuffer, 522 | q: ArrayBuffer, 523 | public_key_fingerprint: string, 524 | encrypted_data: ArrayBuffer, 525 | }; 526 | 527 | export type Set_client_DH_params = { 528 | nonce: Uint32Array, 529 | server_nonce: Uint32Array, 530 | encrypted_data: ArrayBuffer, 531 | }; 532 | 533 | export type Rpc_drop_answer = { 534 | req_msg_id: string, 535 | }; 536 | 537 | export type Get_future_salts = { 538 | num: number, 539 | }; 540 | 541 | export type Ping = { 542 | ping_id: string, 543 | }; 544 | 545 | export type Ping_delay_disconnect = { 546 | ping_id: string, 547 | disconnect_delay: number, 548 | }; 549 | 550 | export type Http_wait = { 551 | max_delay: number, 552 | wait_after: number, 553 | max_wait: number, 554 | }; 555 | 556 | export type Destroy_auth_key = { 557 | }; 558 | 559 | export type Destroy_session = { 560 | session_id: string, 561 | }; 562 | 563 | export interface MethodDeclMap { 564 | 'req_pq': { req: Req_pq, res: ResPQ }, 565 | 'req_pq_multi': { req: Req_pq_multi, res: ResPQ }, 566 | 'req_DH_params': { req: Req_DH_params, res: Server_DH_Params }, 567 | 'set_client_DH_params': { req: Set_client_DH_params, res: Set_client_DH_params_answer }, 568 | 'rpc_drop_answer': { req: Rpc_drop_answer, res: RpcDropAnswer }, 569 | 'get_future_salts': { req: Get_future_salts, res: FutureSalts }, 570 | 'ping': { req: Ping, res: Pong }, 571 | 'ping_delay_disconnect': { req: Ping_delay_disconnect, res: Pong }, 572 | 'http_wait': { req: Http_wait, res: any }, 573 | 'destroy_auth_key': { req: Destroy_auth_key, res: DestroyAuthKeyRes }, 574 | 'destroy_session': { req: Destroy_session, res: DestroySessionRes }, 575 | } 576 | -------------------------------------------------------------------------------- /src/transport/abstract.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import Transport from './abstract'; 3 | import configMock from '../mock/transport_config'; 4 | import { PlainMessage } from '../message'; 5 | 6 | test('Transport | constructor', () => { 7 | const tr = new Transport(() => {}, configMock); 8 | 9 | expect(tr.cfg.dc).toEqual(configMock.dc); 10 | expect(tr.cfg.thread).toEqual(configMock.thread); 11 | 12 | let raised = 0; 13 | 14 | try { 15 | tr.send(new PlainMessage(new Uint32Array(10))); 16 | } catch (e) { 17 | expect(e.message.length).toBeGreaterThan(0); 18 | raised++; 19 | } 20 | 21 | expect(raised).toBe(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/transport/abstract.ts: -------------------------------------------------------------------------------- 1 | import { PlainMessage, EncryptedMessage, ErrorMessage } from '../message'; 2 | import { Transports } from '../client/types'; 3 | 4 | /** Generic config for mtproto transport classes */ 5 | export type TransportConfig = { 6 | dc: number, 7 | thread: number, 8 | host: string, 9 | test: boolean, 10 | ssl: boolean, 11 | debug: boolean, 12 | transport: Transports, 13 | }; 14 | 15 | export type TransportState = 'connected' | 'disconnected' | 'waiting'; 16 | export type TransportCallback = (cfg: TransportConfig, msg: TransportState | ErrorMessage | EncryptedMessage | PlainMessage) => void; 17 | 18 | /** 19 | * Abstract class for all mtproto transport classes 20 | */ 21 | export default class Transport { 22 | /** Instance transport */ 23 | transport = ''; 24 | 25 | /** Message listener */ 26 | pass: TransportCallback; 27 | 28 | /** Transport config */ 29 | cfg: TransportConfig; 30 | 31 | /** 32 | * Creates abstract transport object 33 | */ 34 | constructor(pass: TransportCallback, cfg: TransportConfig) { 35 | this.pass = pass; 36 | this.cfg = cfg; 37 | } 38 | 39 | send(_msg: PlainMessage | EncryptedMessage) { 40 | throw new Error('Unable to send packet with generic transport'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/transport/http.test.ts: -------------------------------------------------------------------------------- 1 | import Http from './http'; 2 | import configMock from '../mock/transport_config'; 3 | import { PlainMessage, ErrorMessage } from '../message'; 4 | import { TransportCallback } from './abstract'; 5 | import plainMock from '../mock/message_plain'; 6 | 7 | test('Transport | http plain call', () => { 8 | const async = new Promise((resolve) => { 9 | const receiver: TransportCallback = (cfg, message) => { 10 | if (!(message instanceof PlainMessage)) throw new Error('Should receive plain message'); 11 | 12 | expect(cfg).toBe(configMock); 13 | expect(message instanceof PlainMessage).toBeTruthy(); 14 | 15 | resolve(message); 16 | }; 17 | 18 | const http = new Http(receiver, configMock); 19 | http.send(plainMock); 20 | }); 21 | 22 | return async.then((message) => { 23 | expect(message.nonce).toEqual(plainMock.nonce); 24 | }); 25 | }); 26 | 27 | test('Transport | http plain error', () => { 28 | const async = new Promise((resolve) => { 29 | const errMsg = new PlainMessage(plainMock.buf.slice(0)); 30 | 31 | errMsg.buf[0] = 0xFF; 32 | 33 | const receiver: TransportCallback = (cfg, message) => { 34 | expect(cfg).toBe(configMock); 35 | resolve(message as ErrorMessage); 36 | }; 37 | 38 | const http = new Http(receiver, configMock); 39 | http.send(errMsg); 40 | }); 41 | 42 | return async.then((message) => { 43 | expect(message instanceof ErrorMessage).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/transport/http.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlainMessage, EncryptedMessage, bytesToMessage, ErrorMessage, 3 | } from '../message'; 4 | import Transport from './abstract'; 5 | import { ab2i } from '../serialization'; 6 | 7 | /** 8 | * Http is a wrapper for XMLHttpRequest for sending serialized type language messages to HTTP MTProto server. 9 | * @extends Transport 10 | */ 11 | export default class Http extends Transport { 12 | /** Instance transport */ 13 | transport = 'http'; 14 | 15 | /** Last plain message nonce */ 16 | lastNonce: string | null = null; 17 | 18 | /** 19 | * Method sends bytes to server via http. 20 | */ 21 | send(msg: PlainMessage | EncryptedMessage) { 22 | const req = new XMLHttpRequest(); 23 | 24 | req.open('POST', `http${this.cfg.ssl ? 's' : ''}://${this.cfg.host}/apiw1${this.cfg.test ? '_test' : ''}`); 25 | req.responseType = 'arraybuffer'; 26 | 27 | // todo: error handling 28 | // req.addEventListener('error', () => { 29 | // this.pass(this.cfg, 0); 30 | // }); 31 | 32 | req.onreadystatechange = () => { 33 | if (req.readyState !== 4) return; 34 | if (req.status >= 200 && req.status < 300) { 35 | const resMsg = bytesToMessage(ab2i(req.response)); 36 | 37 | this.pass(this.cfg, resMsg); 38 | } 39 | 40 | if (req.status === 404) { 41 | const resMsg = new ErrorMessage(0xfffffe53); 42 | 43 | this.pass(this.cfg, resMsg); 44 | } 45 | }; 46 | 47 | req.send(msg.arrayBuffer); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/transport/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Http } from './http'; 2 | export { default as Socket } from './socket'; 3 | -------------------------------------------------------------------------------- /src/transport/protocol/index.ts: -------------------------------------------------------------------------------- 1 | export { wrap, unWrap, HEADER } from './intermediate'; 2 | export { default as Obfuscation } from './obfuscation'; 3 | 4 | export type MTProtoTransport = 'intermediate'; // deprecated: | 'abridged' |'intermediate_padded' | 'full'; 5 | -------------------------------------------------------------------------------- /src/transport/protocol/intermediate.test.ts: -------------------------------------------------------------------------------- 1 | import { randomize } from '../../serialization'; 2 | import { wrap, unWrap } from './intermediate'; 3 | 4 | test('transport | intermediate', () => { 5 | const payload = new Uint32Array(10); 6 | randomize(payload); 7 | 8 | const enveloped = wrap(payload); 9 | expect(enveloped[0]).toEqual(0x28000000); 10 | expect(enveloped.subarray(1)).toEqual(payload); 11 | 12 | const unenveloped = unWrap(enveloped); 13 | expect(unenveloped).toEqual(payload); 14 | }); 15 | 16 | test('transport | intermediate long', () => { 17 | const payload = new Uint32Array(258); 18 | randomize(payload); 19 | 20 | const enveloped = wrap(payload); 21 | expect(enveloped[0]).toEqual(0x08040000); 22 | expect(enveloped.subarray(1)).toEqual(payload); 23 | 24 | const unenveloped = unWrap(enveloped); 25 | expect(unenveloped).toEqual(payload); 26 | }); 27 | -------------------------------------------------------------------------------- /src/transport/protocol/intermediate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Intermediate MTProto Transport Protocol 3 | * Ref: https://core.telegram.org/mtproto/mtproto-transports#intermediate 4 | */ 5 | 6 | /** 7 | * Protocol header 8 | */ 9 | export const HEADER = 0xeeeeeeee; 10 | 11 | /** 12 | * Wraps message at envelope 13 | */ 14 | export function wrap(payload: Uint32Array): Uint32Array { 15 | const len = payload.byteLength; 16 | const enveloped = new Uint32Array(payload.length + 1); 17 | 18 | // quick ack 19 | // len |= 1 << 31; 20 | 21 | // serialize as int32 22 | enveloped[0] = ( 23 | (len & 0xFF) << 24 24 | ^ ((len >> 8) & 0xFF) << 16 25 | ^ ((len >> 16) & 0xFF) << 8 26 | ^ (len >> 24) & 0xFF 27 | ) >>> 0; 28 | 29 | 30 | for (let i = 1; i <= payload.length; i++) enveloped[i] = payload[i - 1]; 31 | 32 | return enveloped; 33 | } 34 | 35 | /** 36 | * Unwraps incoming bytes to type language message 37 | */ 38 | export function unWrap(data: Uint32Array): Uint32Array { 39 | return data.subarray(1); 40 | } 41 | -------------------------------------------------------------------------------- /src/transport/protocol/obfuscation.test.ts: -------------------------------------------------------------------------------- 1 | import Obfuscation from './obfuscation'; 2 | 3 | test('transport | length', () => { 4 | const obf = new Obfuscation(); 5 | 6 | const initpayload = obf.init(0); 7 | 8 | expect(initpayload.length).toBe(16); 9 | expect(initpayload[0]).not.toBe(0x00); 10 | }); 11 | 12 | test('transport | obfuscation init', () => { 13 | const obf = new Obfuscation(); 14 | 15 | const initpayload = obf.init( 16 | 0xefefefef, 17 | new Uint32Array([ 18 | 0xffabcdef, 0x07db79b8, 0xb57d3b12, 0x9f161c25, 0xeaf1632e, 0x36d15f06, 0xa71cf4ed, 0x8e18ef11, 19 | 0xc8719c71, 0xe1c2e66c, 0x91d96d97, 0x6dae9c45, 0x6988cad6, 0xfd0efd51, 0x8aa7cdb3, 0xfeff2430, 20 | ]), 21 | ); 22 | 23 | expect(initpayload).toEqual( 24 | new Uint32Array([ 25 | 0xffabcdef, 0x07db79b8, 0xb57d3b12, 0x9f161c25, 0xeaf1632e, 0x36d15f06, 0xa71cf4ed, 0x8e18ef11, 26 | 0xc8719c71, 0xe1c2e66c, 0x91d96d97, 0x6dae9c45, 0x6988cad6, 0xfd0efd51, 0xcbb56a50, 0x590e498f, 27 | ]), 28 | ); 29 | }); 30 | 31 | test('transport | obfuscation rounds', () => { 32 | const obf = new Obfuscation(); 33 | const zeroed = new Uint32Array(4); 34 | 35 | obf.init( 36 | 0xefefefef, 37 | new Uint32Array([ 38 | 0xffabcdef, 0x76d251b6, 0x65958c01, 0x50cd95d0, 0x474e89ca, 0x22b36c94, 0xc6ab68a3, 0x14b2609f, 39 | 0xf02d6f8d, 0x6b035426, 0x09b2894c, 0x29cfadde, 0x59427f1b, 0x322189ea, 0x429bf094, 0xfeff6035, 40 | ]), 41 | ); 42 | 43 | expect(obf.encode(zeroed)).toEqual(new Uint32Array([0xf5d5e342, 0xbfe5dae9, 0xc5e2453b, 0x56696082])); 44 | expect(obf.encode(zeroed)).toEqual(new Uint32Array([0xf81d3d98, 0x0a3c9b56, 0x500b5fa5, 0x92af2fa2])); 45 | expect(obf.encode(zeroed)).toEqual(new Uint32Array([0xa8f13906, 0x1e47bf70, 0xb119d1e5, 0x15422956])); 46 | 47 | expect(obf.decode(zeroed)).toEqual(new Uint32Array([0x47fd70eb, 0x60c6fc24, 0x76cdd89a, 0xca98c4cd])); 48 | expect(obf.decode(zeroed)).toEqual(new Uint32Array([0xe8eaa6ad, 0xd12249ba, 0x8e2dd055, 0xd4dcffdb])); 49 | expect(obf.decode(zeroed)).toEqual(new Uint32Array([0xbc251bcf, 0xf191ac7e, 0x8fc1a7ff, 0x4d25be15])); 50 | }); 51 | 52 | test('transport | obfuscation real', () => { 53 | const obf = new Obfuscation(); 54 | 55 | const initp = obf.init( 56 | 4008636142, 57 | new Uint32Array([ 58 | 4289449455, 3452546079, 1788317955, 2186783659, 320254335, 2679080564, 795180033, 2779527514, 59 | 1148959518, 736706189, 3377455170, 2985956257, 1123921776, 2235831598, 1275799781, 4248711487, 60 | ]), 61 | ); 62 | 63 | expect(initp).toEqual(new Uint32Array([ 64 | 4289449455, 3452546079, 1788317955, 2186783659, 320254335, 2679080564, 795180033, 2779527514, 65 | 1148959518, 736706189, 3377455170, 2985956257, 1123921776, 2235831598, 2220500043, 3775205634, 66 | ])); 67 | 68 | expect(obf.encode( 69 | new Uint32Array([671088640, 0, 0, 77873706, 3853029726, 335544320, 4052647614, 3755301650, 592405371, 3797949043, 1402297070]), 70 | )).toEqual( 71 | new Uint32Array([2008889190, 3509792512, 4091984948, 3521999420, 2529971718, 1205528763, 1158985946, 3976017160, 2076128840, 3080983806, 2286077413]), 72 | ); 73 | 74 | expect(obf.decode(new Uint32Array([ 75 | 3565710870, 766551384, 374819226, 1787925218, 1438747325, 967937315, 1623912716, 2160422075, 458451121, 1874782247, 2745714511, 2700870023, 76 | 161742505, 1347101544, 3376352641, 1539215727, 2184218714, 939100834, 2820524541, 3281484128, 3612395323, 1848093100, 398542193, 4115272163, 77 | ]))).toEqual(new Uint32Array([ 78 | 1543503872, 0, 0, 26242939, 3886584158, 1207959552, 1663309317, 3755301650, 592405371, 3797949043, 1402297070, 1027755695, 2135311949, 79 | 4083347418, 1657121488, 136437999, 1669650332, 3439329280, 365212956, 33554432, 43994017, 1829802646, 560719980, 36418755, 80 | ])); 81 | 82 | expect(obf.encode(new Uint32Array([ 83 | 1409351680, 0, 0, 70672387, 3869806942, 1073807360, 3202618071, 3755301650, 592405371, 3797949043, 1402297070, 1027755695, 2135311949, 84 | 4083347418, 1657121488, 72641170, 1291845632, 73842036, 2164260864, 560719980, 36418755, 4261413120, 1753347076, 3386287094, 19513172, 85 | 2558587879, 412040485, 2271866144, 3966353970, 4048855205, 2660874261, 4020745895, 793715613, 2238751313, 2919011912, 3624684386, 3548187467, 86 | 1130638591, 3351311194, 2431808512, 1930060847, 618172853, 2070217423, 3624798428, 484182776, 1955608522, 1111368254, 241628998, 1272946250, 87 | 1025562619, 3057733875, 1784312177, 4248472052, 1141020165, 3742838675, 3192907011, 3409833521, 1496641646, 1068361129, 926712687, 2482178215, 88 | 497924140, 2499582337, 3571737140, 601735830, 3861368028, 220579825, 4248810120, 3375258187, 4224623473, 3903275922, 1786253618, 1973256157, 89 | 2137854466, 2492982480, 3176117278, 2908993134, 3238478242, 4184997019, 1580782463, 4106181212, 1115171113, 1172500778, 2722345488, 4049367689, 90 | 2702742698, 91 | ]))).toEqual(new Uint32Array([ 92 | 2933758984, 3076772971, 3273730404, 1272467913, 1947033079, 4004041343, 214513336, 2931120849, 1329742486, 3320522041, 1622095874, 1468018437, 93 | 3138923650, 1306503232, 4165922476, 3334540055, 3277938038, 3116881664, 2506302216, 4062891733, 3883149607, 2929703082, 4258744472, 442795141, 94 | 2616202819, 2940217043, 1040114950, 2119475754, 2822538337, 4125978214, 82947669, 1565639524, 585964677, 3822822402, 3157028117, 751086742, 95 | 1139952331, 115603980, 1236812231, 2019074651, 4221551635, 2664048697, 2113442491, 364496710, 3864335587, 2472242944, 2248281848, 3013608888, 96 | 981677968, 2526156500, 1904063347, 4264737492, 4198691072, 2991100821, 1419956102, 1593042988, 1602918114, 3293224993, 2453079527, 3845468421, 97 | 3968420646, 1996064586, 3331745222, 1981367209, 4191140963, 57025877, 1080372714, 2537970219, 2401826148, 3552611344, 1147844775, 618273734, 98 | 480378443, 416885094, 2321304757, 3981169777, 3962474541, 4096157063, 2778246840, 2415751217, 3198724611, 2860169670, 3175692240, 2443224797, 99 | 4236927080, 3447377821, 100 | ])); 101 | 102 | expect( 103 | obf.encode( 104 | new Uint32Array([ 105 | 2348875776, 0, 0, 80736540, 3869806942, 2013331456, 526320885, 3755301650, 592405371, 3797949043, 1402297070, 1027755695, 2135311949, 106 | 4083347418, 1657121488, 4266656000, 2969343097, 2813990996, 3550379277, 1018692917, 1125614469, 2755878047, 2066881117, 3314673065, 107 | 3808812164, 4266725261, 3527968112, 158124639, 3255597124, 676347237, 3783581280, 3599034104, 3674670481, 1602698694, 3153513449, 1504264447, 108 | 1006639820, 116214419, 2322143494, 2815879709, 987876500, 241295355, 420563528, 3432257161, 4240035643, 3706996855, 4269166646, 2340278220, 109 | 648092869, 2771119028, 940149317, 2895106964, 945124724, 3536174676, 578947834, 1216200046, 2117928173, 3868430735, 4029597941, 715684260, 110 | 4274148334, 364385784, 4081669749, 2194523889, 1387050469, 3958251158, 2442241337, 1673071051, 2580685890, 4259991483, 2835587417, 3548847196, 111 | 3150677073, 3532736921, 168164719, 2720525604, 3508837869, 4258151087, 1834947869, 1105394374, 1309219200, 3202676998, 1714469930, 2648106133, 112 | 197240124, 281303751, 2174617750, 1208979965, 3124216217, 4085667971, 1465655262, 980440705, 4118424989, 2744524611, 2457297867, 2558396341, 113 | 333021346, 1701499759, 3041865568, 3858274447, 114 | ]), 115 | ), 116 | ).toEqual( 117 | new Uint32Array([ 118 | 798722726, 4011341536, 4286966340, 2368747607, 1035600324, 601466937, 1697150819, 17044685, 3938003480, 400869975, 2009976707, 2313090146, 119 | 3811973511, 1870931787, 2968545854, 465785826, 612294244, 2173343977, 2742135926, 165719635, 437468914, 1232068292, 4154325744, 574565466, 120 | 1142118994, 3111507152, 1365496553, 173739929, 2971467673, 1458951079, 556831006, 1222142986, 705886902, 1290815168, 3416367036, 2497987921, 121 | 1201507815, 2147383587, 3601632734, 3657651522, 1723810095, 3018551831, 3093034658, 1645389520, 1480743724, 1327414654, 2438517111, 3945386452, 122 | 2645148194, 2047233375, 4158530410, 3577025531, 633682261, 536860384, 2273652164, 3176664111, 1597185823, 2871286829, 3812561599, 2549712077, 123 | 1664736838, 1929433536, 1953520023, 711311794, 488679944, 1024438486, 2058582718, 423401757, 2676673537, 806154661, 196546409, 4006472528, 124 | 4287775770, 1326999937, 2391876748, 618548550, 3661660869, 1226608160, 1570235091, 1322894122, 4259131942, 1063319854, 1040781954, 2028463519, 125 | 2535289713, 1026491231, 3679955260, 1781353782, 470173948, 1568135357, 3526414144, 3801776996, 2857725332, 2526362690, 1733717812, 961183303, 126 | 4011578101, 3880698698, 3055551088, 3135261831, 127 | ]), 128 | ); 129 | }); 130 | 131 | test('transport | obfuscation errors', () => { 132 | let raised = 0; 133 | 134 | const obf = new Obfuscation(); 135 | 136 | try { 137 | obf.encode(new Uint32Array(4)); 138 | } catch (e) { 139 | expect(e.message.length).toBeGreaterThan(0); 140 | raised++; 141 | } 142 | 143 | try { 144 | obf.decode(new Uint32Array(4)); 145 | } catch (e) { 146 | expect(e.message.length).toBeGreaterThan(0); 147 | raised++; 148 | } 149 | 150 | expect(raised).toBe(2); 151 | }); 152 | -------------------------------------------------------------------------------- /src/transport/protocol/obfuscation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | import { CTR } from '@cryptography/aes'; 3 | import { randomize, reverse32 } from '../../serialization'; 4 | 5 | /** 6 | * Obfuscation for MTProto Transport Protocol 7 | * Ref: https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation 8 | */ 9 | export default class Obfuscation { 10 | /** Encription Cipher */ 11 | enc?: CTR; 12 | 13 | /** Decription Cipher */ 14 | dec?: CTR; 15 | 16 | /** 17 | * Creates initialization payload for establishing web-socket connection 18 | */ 19 | init(header: number, randomized?: Uint32Array): Uint32Array { 20 | let initPayload; 21 | 22 | if (randomized) initPayload = randomized; 23 | else { 24 | initPayload = new Uint32Array(16); 25 | randomize(initPayload); 26 | initPayload[0] = 0xFFABCDEF; // avoid collisions 27 | } 28 | 29 | if (header) initPayload[14] = header; 30 | 31 | const reversedPayload = reverse32(initPayload); 32 | 33 | const encKey = initPayload.subarray(2, 10); 34 | const encIv = initPayload.slice(10, 14); 35 | const decKey = reversedPayload.slice(2, 10); 36 | const decIv = reversedPayload.slice(10, 14); 37 | 38 | // to do: typing for aesjs 39 | this.enc = new CTR(encKey, encIv); 40 | this.dec = new CTR(decKey, decIv); 41 | 42 | const encrypted = this.enc.encrypt(initPayload); 43 | 44 | initPayload[14] = encrypted[14]; 45 | initPayload[15] = encrypted[15]; 46 | 47 | return initPayload; 48 | } 49 | 50 | /** 51 | * Obfuscates data 52 | */ 53 | encode(payload: string | Uint32Array | Uint8Array): Uint32Array { 54 | if (!this.enc) throw new Error('Must init first'); 55 | return this.enc.encrypt(payload); 56 | } 57 | 58 | 59 | /** 60 | * Decodes obfuscated data 61 | */ 62 | decode(data: string | Uint32Array | Uint8Array): Uint32Array { 63 | if (!this.dec) throw new Error('Must init first'); 64 | return this.dec.encrypt(data); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/transport/socket.test.ts: -------------------------------------------------------------------------------- 1 | import Socket from './socket'; 2 | import { PlainMessage } from '../message'; 3 | import configMock from '../mock/transport_config'; 4 | import { TransportCallback } from './abstract'; 5 | import plainMock from '../mock/message_plain'; 6 | 7 | test('Transport | socket plain call', () => { 8 | const async = new Promise((resolve) => { 9 | const receiver: TransportCallback = (cfg, message) => { 10 | if (typeof message === 'string') return; // skip service messages 11 | 12 | if (!(message instanceof PlainMessage)) throw new Error('Should receive plain message'); 13 | 14 | expect(cfg).toBe(configMock); 15 | expect(message instanceof PlainMessage).toBeTruthy(); 16 | 17 | resolve(message); 18 | }; 19 | 20 | const socket = new Socket(receiver, configMock); 21 | socket.send(plainMock); 22 | }); 23 | 24 | return async.then((message) => { 25 | expect(message.nonce).toEqual(plainMock.nonce); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/transport/socket.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | import Transport, { TransportConfig, TransportCallback, TransportState } from './abstract'; 3 | import { logs } from '../utils/log'; 4 | import { i2ab, ab2i } from '../serialization'; 5 | import { PlainMessage, bytesToMessage, EncryptedMessage } from '../message'; 6 | import { wrap, unWrap, HEADER, Obfuscation } from './protocol'; 7 | 8 | /** Configuration object for WebSocket transport */ 9 | type SocketConfig = TransportConfig; 10 | 11 | /** Format debug messages if debug flag is enabled */ 12 | const debug = (cfg: SocketConfig, ...rest: any[]) => { 13 | if (cfg.debug) logs('socket')('[dc:', cfg.dc, 'thread:', cfg.thread, ']', ...rest); 14 | }; 15 | 16 | export default class Socket extends Transport { 17 | /** Connection handler */ 18 | ws: WebSocket | undefined; 19 | 20 | /** Pending Requests */ 21 | pending: Array = []; 22 | 23 | /** WebSocket Config */ 24 | cfg: SocketConfig; 25 | 26 | /** Instance transport */ 27 | transport = 'websocket'; 28 | 29 | /** Transport obfuscation */ 30 | obfuscation?: Obfuscation; 31 | 32 | /** Last connect retry */ 33 | lastConnectRetry = 0; 34 | 35 | /** Reconnect timer */ 36 | reconnectTimer?: number; 37 | 38 | /** Request timeout timer */ 39 | requestTimer?: number; 40 | 41 | /** Transport state */ 42 | state: TransportState = 'disconnected'; 43 | 44 | /** 45 | * Creates new web socket handler 46 | */ 47 | constructor(receiver: TransportCallback, cfg: SocketConfig) { 48 | super(receiver, cfg); 49 | 50 | this.cfg = cfg; 51 | this.connect(); 52 | 53 | if (self) { 54 | self.addEventListener('online', () => { 55 | this.connect(); 56 | }); 57 | 58 | self.addEventListener('offline', () => { 59 | if (this.ws) this.ws.close(); 60 | }); 61 | } 62 | } 63 | 64 | notify = (status: TransportState) => { 65 | if (status !== this.state) { 66 | this.pass(this.cfg, status); 67 | this.state = status; 68 | } 69 | }; 70 | 71 | connect = (force?: boolean) => { 72 | if (this.ws && this.ws.readyState <= 1) return; 73 | 74 | const timestamp = Date.now(); 75 | 76 | // forced connect 77 | if (force) { 78 | if (this.reconnectTimer) clearTimeout(this.reconnectTimer); 79 | 80 | // auto connect should be throttled 81 | } else if (timestamp - this.lastConnectRetry < 1000) { 82 | this.reconnectTimer = setTimeout(this.connect as TimerHandler, 3000); 83 | return; 84 | } 85 | 86 | this.reconnectTimer = 0; 87 | 88 | if (navigator.onLine !== false) { 89 | this.lastConnectRetry = timestamp; 90 | 91 | this.ws = new WebSocket(`ws${this.cfg.ssl ? 's' : ''}://${this.cfg.host}/apiws${this.cfg.test ? '_test' : ''}`, 'binary'); 92 | this.ws.binaryType = 'arraybuffer'; 93 | this.ws.onopen = this.handleOpen; 94 | this.ws.onclose = this.handleClose; 95 | this.ws.onmessage = this.handleMessage; 96 | } 97 | }; 98 | 99 | /** 100 | * Handles onopen event at websocket object 101 | */ 102 | handleOpen = () => { 103 | if (!this.ws) return; 104 | 105 | this.obfuscation = new Obfuscation(); 106 | 107 | // notify client 108 | this.notify('connected'); 109 | 110 | // init obfuscation with first packet 111 | const initPayload = this.obfuscation.init(HEADER); 112 | this.ws.send(i2ab(initPayload)); 113 | 114 | // release pending messages 115 | this.releasePending(); 116 | 117 | debug(this.cfg, 'opened'); 118 | }; 119 | 120 | /** 121 | * Handles onclose event at websocket object 122 | */ 123 | handleClose = (_event: CloseEvent) => { 124 | debug(this.cfg, 'closed'); 125 | 126 | // notify client 127 | this.notify('disconnected'); 128 | 129 | // keep connection for 1st threads 130 | if (this.cfg.thread === 1) this.connect(); 131 | }; 132 | 133 | /** 134 | * Handles onmessage event at websocket object 135 | */ 136 | handleMessage = (event: MessageEvent) => { 137 | if (!event.data || !this.obfuscation) return; 138 | 139 | // process message 140 | const data = unWrap(this.obfuscation.decode(ab2i(event.data))); 141 | const msg = bytesToMessage(data); 142 | 143 | // flush request timer 144 | if (this.requestTimer) { 145 | clearTimeout(this.requestTimer); 146 | this.requestTimer = 0; 147 | } 148 | 149 | // notify client 150 | if (this.state !== 'connected') this.notify('connected'); 151 | 152 | // pass message to main client thread 153 | this.pass(this.cfg, msg); 154 | }; 155 | 156 | /** 157 | * Handles request timeout 158 | */ 159 | handleRequestTimout = () => { 160 | debug(this.cfg, 'waiting'); 161 | 162 | // notify client 163 | // this.notify('waiting'); 164 | }; 165 | 166 | /** 167 | * Method sends bytes to server via web socket. 168 | */ 169 | send(msg: PlainMessage | EncryptedMessage) { 170 | // send if socket is ready 171 | if (this.obfuscation && this.ws && this.ws.readyState === 1) { 172 | const frame = this.obfuscation.encode(wrap(msg.buf)); 173 | this.ws.send(i2ab(frame)); 174 | 175 | // delay request timeout handler 176 | if (msg instanceof EncryptedMessage && msg.isContentRelated && !this.requestTimer) { 177 | // this.requestTimer = setTimeout(this.handleRequestTimout as TimerHandler, 5000); 178 | } 179 | 180 | // else: add message to pending quene and reconnect 181 | } else { 182 | this.pending.push(msg); 183 | if (!this.ws || this.ws.readyState !== 0) this.connect(true); 184 | } 185 | } 186 | 187 | /** 188 | * Re-sends pending messages 189 | */ 190 | releasePending() { 191 | if (this.pending) { 192 | const num = this.pending.length; 193 | for (let i = 0; i < num; i += 1) { 194 | const msg = this.pending.shift(); 195 | if (msg) this.send(msg); 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/utils/crc32.test.ts: -------------------------------------------------------------------------------- 1 | import crc32 from './crc32'; 2 | 3 | test('crc32', () => { 4 | expect(crc32('req_pq_multi nonce:int128 = ResPQ')).toBe(0xbe7e8ef1); 5 | }); 6 | -------------------------------------------------------------------------------- /src/utils/crc32.ts: -------------------------------------------------------------------------------- 1 | const premadeTable = (() => { 2 | const table = []; 3 | 4 | let c; 5 | for (let n = 0; n < 256; n += 1) { 6 | c = n; 7 | 8 | for (let k = 0; k < 8; k += 1) { 9 | c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); 10 | } 11 | 12 | table[n] = c; 13 | } 14 | return table; 15 | })(); 16 | 17 | export default function crc32(input: string): number { 18 | let output = 0 ^ (-1); 19 | 20 | for (let i = 0; i < input.length; i += 1) { 21 | output = (output >>> 8) ^ premadeTable[(output ^ input.charCodeAt(i)) & 0xFF]; 22 | } 23 | 24 | return (output ^ (-1)) >>> 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export function log(...args: unknown[]) { 4 | console.log('[main]', ...args); 5 | } 6 | 7 | export function logs(scope: string, prefix: string = ''): (...args: unknown[]) => void { 8 | return (...args: unknown[]) => { 9 | console.log(`${prefix}[${scope}]`, ...args); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": true, 4 | "strict": true, 5 | "strictFunctionTypes": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "importHelpers": false, 9 | "moduleResolution": "node", 10 | "sourceMap": false, 11 | "esModuleInterop": true, 12 | "target": "es5", 13 | "baseUrl": "./src", 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------