├── src ├── node.ts ├── index.ts ├── browser.ts ├── utils.ts ├── client-tcp.ts ├── hash.ts ├── keys.ts ├── params.ts ├── aes.ts ├── address.ts ├── client-ws.ts ├── packet.ts └── client.ts ├── README.md ├── tsconfig.build.json ├── .nycrc.json ├── typedoc.json ├── .gitignore ├── .npmignore ├── tsconfig.json ├── LICENSE ├── package.json └── .eslintrc.json /src/node.ts: -------------------------------------------------------------------------------- 1 | export { ADNLClientTCP } from './client-tcp' 2 | export { ADNLClientWS } from './client-ws' 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | >⚠️ Repo was moved to [tonkite](https://github.com/tonkite/adnl) organization. 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src/**/*.ts"], 4 | "exclude": ["./test"] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ADNLClientTCP } from './client-tcp' 2 | export { ADNLClientWS } from './client-ws' 3 | export { ADNLClient } from './client' 4 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | export { ADNLClientWS } from './client-ws' 2 | export { ADNLClient } from './client' 3 | 4 | export class ADNLClientTCP { 5 | constructor (url: string, peerPublicKey: Uint8Array | string) { 6 | throw new Error('ADNLClientTCP is not available on this platform') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const uintToHex = (uint: number): string => { 2 | const hex = `0${uint.toString(16)}` 3 | 4 | return hex.slice(-(Math.floor(hex.length / 2) * 2)) 5 | } 6 | 7 | const bytesToHex = (bytes: Uint8Array): string => { 8 | return bytes.reduce((acc, uint) => `${acc}${uintToHex(uint)}`, '') 9 | } 10 | 11 | export { bytesToHex } 12 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "check-coverage": true, 4 | "all": true, 5 | "include": [ 6 | "src/**/*.ts" 7 | ], 8 | "exclude": [ 9 | "node_modules/" 10 | ], 11 | "extension": [ 12 | ".ts" 13 | ], 14 | "reporter": [ 15 | "text-summary", 16 | "html" 17 | ], 18 | "report-dir": "./coverage" 19 | } 20 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeExternals": true, 3 | "excludePrivate": true, 4 | "excludeProtected": true, 5 | "excludeInternal": true, 6 | "disableSources": true, 7 | "sort": ["source-order", "alphabetical"], 8 | "readme": "none", 9 | "theme": "markdown", 10 | "plugin": ["typedoc-plugin-merge-modules", "typedoc-plugin-markdown"], 11 | "entryPointStrategy": "expand", 12 | "entryPoints": [ "./src"], 13 | "out": "./docs" 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | t.py 20 | hex.txt 21 | out.boc 22 | 23 | # TON configs 24 | global-config.json 25 | testnet-global.config.json 26 | 27 | # IDEs and editors 28 | /.idea 29 | .project 30 | .classpath 31 | .c9/ 32 | *.launch 33 | .settings/ 34 | *.sublime-workspace 35 | 36 | # IDE - VSCode 37 | .vscode 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src 3 | 4 | # Typescript 5 | tsconfig*.json 6 | dist/tsconfig.build.tsbuildinfo 7 | 8 | # Docs 9 | /docs 10 | typedoc.json 11 | 12 | # Tests 13 | /coverage 14 | /test 15 | .nycrc.json 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | lerna-debug.log* 24 | 25 | # OS 26 | .DS_Store 27 | 28 | # Tests 29 | /coverage 30 | /.nyc_output 31 | 32 | # IDEs and editors 33 | /.idea 34 | .project 35 | .classpath 36 | .c9/ 37 | *.launch 38 | .settings/ 39 | *.sublime-workspace 40 | .eslintrc.json 41 | 42 | # IDE - VSCode 43 | .vscode 44 | -------------------------------------------------------------------------------- /src/client-tcp.ts: -------------------------------------------------------------------------------- 1 | import { ADNLClient } from './client' 2 | import { Socket } from 'net' 3 | 4 | class ADNLClientTCP extends ADNLClient { 5 | constructor (url: string, peerPublicKey: Uint8Array | string) { 6 | super(new Socket(), url, peerPublicKey) 7 | 8 | this.socket 9 | .on('connect', this.onConnect.bind(this)) 10 | .on('ready', this.onHandshake.bind(this)) 11 | .on('close', this.onClose.bind(this)) 12 | .on('data', this.onData.bind(this)) 13 | .on('error', this.onError.bind(this)) 14 | } 15 | } 16 | 17 | export { ADNLClientTCP } 18 | -------------------------------------------------------------------------------- /src/hash.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from './utils' 2 | import { 3 | SHA256, 4 | SHA512, 5 | enc 6 | } from 'crypto-js' 7 | 8 | const sha256 = (bytes: Uint8Array): Uint8Array => { 9 | const hex = bytesToHex(bytes) 10 | const words = enc.Hex.parse(hex) 11 | const hash = SHA256(words).toString() 12 | 13 | return new Uint8Array(Buffer.from(hash, 'hex')) 14 | } 15 | 16 | const sha512 = (bytes: Uint8Array): Uint8Array => { 17 | const hex = bytesToHex(bytes) 18 | const words = enc.Hex.parse(hex) 19 | const hash = SHA512(words).toString() 20 | 21 | return new Uint8Array(Buffer.from(hash, 'hex')) 22 | } 23 | 24 | export { 25 | sha256, 26 | sha512 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ES2020", 6 | "lib": [ 7 | "ES2020", 8 | "ESNEXT" 9 | ], 10 | "declaration": true, 11 | "removeComments": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true, 16 | "sourceMap": true, 17 | "outDir": "./dist", 18 | "baseUrl": "./src", 19 | "incremental": true 20 | }, 21 | "ts-node": { 22 | "transpileOnly": true 23 | }, 24 | "include": ["./**/*.ts"], 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getPublicKey, 3 | getSharedSecret 4 | } from '@noble/ed25519' 5 | import { randomBytes } from 'tweetnacl' 6 | 7 | class ADNLKeys { 8 | private _peer: Uint8Array 9 | 10 | private _public: Uint8Array 11 | 12 | private _shared: Uint8Array 13 | 14 | constructor (peerPublicKey: Uint8Array) { 15 | this._peer = peerPublicKey 16 | } 17 | 18 | public get public (): Uint8Array { 19 | return new Uint8Array(this._public) 20 | } 21 | 22 | public get shared (): Uint8Array { 23 | return new Uint8Array(this._shared) 24 | } 25 | 26 | public async generate () { 27 | const privateKey = randomBytes(32) 28 | const publicKey = await getPublicKey(privateKey) 29 | const shared = await getSharedSecret(privateKey, this._peer) 30 | 31 | this._public = publicKey 32 | this._shared = shared 33 | } 34 | } 35 | 36 | export { ADNLKeys } 37 | -------------------------------------------------------------------------------- /src/params.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from './hash' 2 | import { randomBytes } from 'tweetnacl' 3 | 4 | class ADNLAESParams { 5 | private _bytes: Uint8Array 6 | 7 | constructor () { 8 | this._bytes = new Uint8Array(randomBytes(160)) 9 | } 10 | 11 | public get bytes (): Uint8Array { 12 | return new Uint8Array(this._bytes) 13 | } 14 | 15 | public get rxKey (): Uint8Array { 16 | return this.bytes.slice(0, 32) 17 | } 18 | 19 | public get txKey (): Uint8Array { 20 | return this.bytes.slice(32, 64) 21 | } 22 | 23 | public get rxNonce (): Uint8Array { 24 | return this.bytes.slice(64, 80) 25 | } 26 | 27 | public get txNonce (): Uint8Array { 28 | return this.bytes.slice(80, 96) 29 | } 30 | 31 | public get padding (): Uint8Array { 32 | return this.bytes.slice(96, 160) 33 | } 34 | 35 | public get hash (): Uint8Array { 36 | const hash = sha256(this._bytes) 37 | 38 | return hash 39 | } 40 | } 41 | 42 | export { ADNLAESParams } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TonStack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/aes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModeOfOperation, 3 | Counter 4 | } from 'aes-js' 5 | 6 | class CipherBase { 7 | protected cipher: ModeOfOperation.ModeOfOperationCTR 8 | 9 | constructor (key: Uint8Array, iv: Uint8Array) { 10 | this.cipher = new ModeOfOperation.ctr(key, new Counter(iv)) 11 | } 12 | 13 | public final (): Uint8Array { 14 | return new Uint8Array([]) 15 | } 16 | } 17 | 18 | class Cipher extends CipherBase { 19 | constructor (key: Uint8Array, iv: Uint8Array) { 20 | super(key, iv) 21 | } 22 | 23 | public update (data: Uint8Array): Uint8Array { 24 | const result = this.cipher.encrypt(data) 25 | 26 | return result 27 | } 28 | } 29 | 30 | class Decipher extends Cipher { 31 | constructor (key: Uint8Array, iv: Uint8Array) { 32 | super(key, iv) 33 | } 34 | 35 | public update (data: Uint8Array): Uint8Array { 36 | const result = this.cipher.decrypt(data) 37 | 38 | return result 39 | } 40 | } 41 | 42 | const createCipheriv = (_algo: string, key: Uint8Array, iv: Uint8Array): Cipher => { 43 | return new Cipher(key, iv) 44 | } 45 | 46 | const createDecipheriv = (_algo: string, key: Uint8Array, iv: Uint8Array): Decipher => { 47 | return new Decipher(key, iv) 48 | } 49 | 50 | export { 51 | Cipher, 52 | Decipher, 53 | createCipheriv, 54 | createDecipheriv 55 | } 56 | -------------------------------------------------------------------------------- /src/address.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from './hash' 2 | 3 | class ADNLAddress { 4 | private _publicKey: Uint8Array 5 | 6 | constructor (publicKey: Uint8Array | string) { 7 | const value = ADNLAddress.isBytes(publicKey) ? publicKey : (publicKey as string).trim() 8 | 9 | if (ADNLAddress.isBytes(value)) { 10 | this._publicKey = value as Uint8Array 11 | } else if (ADNLAddress.isHex(value)) { 12 | this._publicKey = new Uint8Array(Buffer.from(value as string, 'hex')) 13 | } else if (ADNLAddress.isBase64(value)) { 14 | this._publicKey = new Uint8Array(Buffer.from(value as string, 'base64')) 15 | } 16 | 17 | if (this._publicKey.length !== 32) { 18 | throw new Error('ADNLAddress: Bad peer public key. Must contain 32 bytes.') 19 | } 20 | } 21 | 22 | public get publicKey (): Uint8Array { 23 | return new Uint8Array(this._publicKey) 24 | } 25 | 26 | public get hash (): Uint8Array { 27 | const typeEd25519 = new Uint8Array([ 0xc6, 0xb4, 0x13, 0x48 ]) 28 | const key = new Uint8Array([ ...typeEd25519, ...this._publicKey ]) 29 | 30 | return sha256(key) 31 | } 32 | 33 | private static isHex (data: any): boolean { 34 | const re = /^[a-fA-F0-9]+$/ 35 | 36 | return typeof data === 'string' && re.test(data) 37 | } 38 | 39 | private static isBase64 (data: any): boolean { 40 | // eslint-disable-next-line no-useless-escape 41 | const re = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ 42 | 43 | return typeof data === 'string' && re.test(data) 44 | } 45 | 46 | private static isBytes (data: any): boolean { 47 | return ArrayBuffer.isView(data) 48 | } 49 | } 50 | 51 | export { ADNLAddress } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adnl", 3 | "version": "0.0.13", 4 | "description": "ADNL JavaScript implementation", 5 | "main": "./dist/node.js", 6 | "browser": "./dist/browser.js", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "test": "mocha './test/**/*.test.ts' --require ts-node/register", 10 | "coverage": "nyc --all mocha './test/**/*.test.ts' --require ts-node/register", 11 | "build:doc": "typedoc" 12 | }, 13 | "author": "TonStack", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 17 | "@types/aes-js": "^3.1.1", 18 | "@types/chai": "^4.3.0", 19 | "@types/crypto-js": "^4.1.1", 20 | "@types/mocha": "^9.1.0", 21 | "@types/sinon": "^10.0.11", 22 | "chai": "^4.3.6", 23 | "eslint": "^8.10.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-config-airbnb-typescript": "^16.1.0", 26 | "eslint-import-resolver-typescript": "^2.5.0", 27 | "eslint-plugin-import": "^2.25.4", 28 | "mocha": "^9.2.1", 29 | "nyc": "^15.1.0", 30 | "sinon": "^13.0.1", 31 | "ts-node": "^10.7.0", 32 | "typedoc": "^0.22.13", 33 | "typedoc-plugin-markdown": "^3.11.14", 34 | "typedoc-plugin-merge-modules": "^3.1.0", 35 | "typescript": "^4.6.3" 36 | }, 37 | "types": "./dist/index.d.ts", 38 | "directories": { 39 | "doc": "docs", 40 | "test": "test" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/tonstack/adnl-js.git" 45 | }, 46 | "keywords": [ 47 | "adnl" 48 | ], 49 | "bugs": { 50 | "url": "https://github.com/tonstack/adnl-js/issues" 51 | }, 52 | "homepage": "https://github.com/tonstack/adnl-js#readme", 53 | "dependencies": { 54 | "@noble/ed25519": "^1.6.1", 55 | "aes-js": "^3.1.2", 56 | "buffer": "^6.0.3", 57 | "crypto-js": "^4.1.1", 58 | "events": "^3.3.0", 59 | "isomorphic-ws": "^5.0.0", 60 | "tweetnacl": "^1.0.3", 61 | "ws": "^8.8.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/client-ws.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'isomorphic-ws' 2 | import { 3 | ADNLClient, 4 | ADNLClientState 5 | } from './client' 6 | import { ADNLPacket } from './packet' 7 | 8 | class ADNLClientWS extends ADNLClient { 9 | private url: string 10 | 11 | constructor (url: string, peerPublicKey: Uint8Array | string) { 12 | super(null, url, peerPublicKey) 13 | 14 | this.url = url 15 | } 16 | 17 | private async parse (message: any): Promise { 18 | let data: Buffer 19 | 20 | switch (true) { 21 | case typeof message === 'string': 22 | data = Buffer.from(message) 23 | 24 | break 25 | case message instanceof Buffer: 26 | data = message 27 | break 28 | case message instanceof ArrayBuffer: 29 | data = Buffer.from(message) 30 | break 31 | default: 32 | const blob = message as unknown as Blob 33 | 34 | data = Buffer.from(await blob.arrayBuffer()) 35 | 36 | break 37 | } 38 | 39 | return data 40 | } 41 | 42 | protected onHandshake (): void { 43 | this.socket.send(this.handshake) 44 | } 45 | 46 | public async connect (): Promise { 47 | await this.onBeforeConnect() 48 | 49 | this.socket = new WebSocket(this.url) 50 | 51 | this.socket.onopen = () => { 52 | this.onConnect() 53 | this.onHandshake() 54 | } 55 | 56 | this.socket.onmessage = async (event: WebSocket.MessageEvent) => { 57 | const data = await this.parse(event.data) 58 | 59 | this.onData(data) 60 | } 61 | 62 | this.socket.onclose = this.onClose.bind(this) 63 | this.socket.onerror = this.onError.bind(this) 64 | } 65 | 66 | public end (): void { 67 | if ( 68 | this.state === ADNLClientState.CLOSING 69 | || this.state === ADNLClientState.CLOSED 70 | ) { 71 | return undefined 72 | } 73 | 74 | this.socket.close() 75 | } 76 | 77 | public write (data: Buffer): void { 78 | const packet = new ADNLPacket(data) 79 | const encrypted = this.encrypt(packet.data) 80 | 81 | this.socket.send(encrypted) 82 | } 83 | } 84 | 85 | export { ADNLClientWS } 86 | -------------------------------------------------------------------------------- /src/packet.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from './hash' 2 | import { randomBytes } from 'tweetnacl' 3 | 4 | const PACKET_MIN_SIZE = 4 + 32 + 32 // size + nonce + hash 5 | 6 | class ADNLPacket { 7 | private _payload: Buffer 8 | 9 | private _nonce: Buffer 10 | 11 | constructor (payload: Buffer, nonce: Buffer = Buffer.from(randomBytes(32))) { 12 | this._payload = payload 13 | this._nonce = nonce// Buffer.from('8e561596e259180c85fccccbc30420d3d7e3c6808819aaea8c0e22157601f69f', 'hex')//nonce 14 | } 15 | 16 | public get payload (): Buffer { 17 | return this._payload 18 | } 19 | 20 | public get nonce (): Buffer { 21 | return this._nonce 22 | } 23 | 24 | public get hash (): Buffer { 25 | const value = new Uint8Array([ ...this.nonce, ...this.payload ]) 26 | 27 | return Buffer.from(sha256(value)) 28 | } 29 | 30 | public get size (): Buffer { 31 | const buffer = new ArrayBuffer(4) 32 | const view = new DataView(buffer) 33 | 34 | view.setUint32(0, this._payload.length + 32 + 32, true) 35 | 36 | return Buffer.from(view.buffer) 37 | } 38 | 39 | public get data (): Buffer { 40 | return Buffer.concat([ this.size, this.nonce, this.payload, this.hash ]) 41 | } 42 | 43 | public get length (): number { 44 | return 4 + 32 + this._payload.length + 32 45 | } 46 | 47 | public static parse (data: Buffer): ADNLPacket | null { 48 | const packet = { cursor: 0, data } 49 | 50 | if (packet.data.byteLength < 4) { 51 | return null 52 | } 53 | 54 | const size = packet.data.slice(0, packet.cursor += 4).readUint32LE(0) 55 | 56 | if (packet.data.byteLength - 4 < size) { 57 | return null 58 | } 59 | 60 | const nonce = packet.data.slice(packet.cursor, packet.cursor += 32) 61 | const payload = packet.data.slice(packet.cursor, packet.cursor += (size - (32 + 32))) 62 | const hash = packet.data.slice(packet.cursor, packet.cursor += 32) 63 | const target = Buffer.from(sha256(new Uint8Array([ ...nonce, ...payload ]))) 64 | 65 | if (!hash.equals(target)) { 66 | throw new Error('ADNLPacket: Bad packet hash.') 67 | } 68 | 69 | return new ADNLPacket(payload, nonce) 70 | } 71 | } 72 | 73 | export { 74 | ADNLPacket, 75 | PACKET_MIN_SIZE 76 | } 77 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json", 5 | "tsconfigRootDir": "./", 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "extends": [ 13 | "airbnb-base", 14 | "airbnb-typescript/base", 15 | "plugin:import/errors", 16 | "plugin:import/warnings", 17 | "plugin:import/typescript" 18 | ], 19 | "ignorePatterns": [".eslintrc.json"], 20 | "root": true, 21 | "env": { 22 | "node": true, 23 | "jest": true 24 | }, 25 | "settings": { 26 | "import/resolver": { 27 | "typescript": { 28 | "extensions": [".ts", ".tsx"], 29 | "moduleDirectory": ["src", "node_modules"] 30 | }, 31 | "node": {} 32 | } 33 | }, 34 | "rules": { 35 | "no-console": "off", 36 | "no-shadow": "off", 37 | "no-bitwise": "off", 38 | "no-unused-vars": "off", 39 | // "indent": ["error", 4, { 40 | // "SwitchCase": 1 41 | // }], 42 | // "semi": ["error", "never"], 43 | // "comma-dangle": ["error", "never"], 44 | "array-bracket-spacing": ["error", "always"], 45 | "object-curly-newline": ["error", { "multiline": true }], 46 | // "object-curly-spacing": ["error", "always"], 47 | "no-useless-constructor": "off", 48 | "@typescript-eslint/indent": ["error", 4, { 49 | "SwitchCase": 1 50 | }], 51 | "@typescript-eslint/space-before-function-paren": ["error", "always"], 52 | "@typescript-eslint/semi": ["error", "never"], 53 | "@typescript-eslint/comma-dangle": ["error", "never"], 54 | "@typescript-eslint/object-curly-spacing": ["error", "always"], 55 | "@typescript-eslint/no-shadow": ["error"], 56 | "@typescript-eslint/no-useless-constructor": "off", 57 | "@typescript-eslint/interface-name-prefix": "off", 58 | "@typescript-eslint/explicit-function-return-type": "off", 59 | "@typescript-eslint/explicit-module-boundary-types": ["error", { 60 | "allowedNames": ["transform"] 61 | }], 62 | "@typescript-eslint/no-unused-vars": ["error", { 63 | "argsIgnorePattern": "^_" 64 | }], 65 | "@typescript-eslint/no-explicit-any": "off", 66 | "import/prefer-default-export": "off", 67 | "import/no-default-export": "error", 68 | "space-before-function-paren": ["error", "always"], 69 | "lines-between-class-members": ["error", "always", { 70 | "exceptAfterSingleLine": true 71 | }], 72 | "no-underscore-dangle": "off", 73 | "arrow-parens": [2, "as-needed", { 74 | "requireForBlockBody": true 75 | }], 76 | "import/no-extraneous-dependencies": ["error", { 77 | "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/*.e2e-spec.ts", "./webpack.config.json"] 78 | }], 79 | "import/extensions": ["error", "ignorePackages", { 80 | "js": "never", 81 | "mjs": "never", 82 | "jsx": "never", 83 | "ts": "never", 84 | "tsx": "never" 85 | }] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import { 3 | createCipheriv, 4 | createDecipheriv, 5 | Cipher, 6 | Decipher 7 | } from './aes' 8 | import { 9 | ADNLPacket, 10 | PACKET_MIN_SIZE 11 | } from './packet' 12 | import { ADNLAESParams } from './params' 13 | import { ADNLAddress } from './address' 14 | import { ADNLKeys } from './keys' 15 | 16 | enum ADNLClientState { 17 | CONNECTING, 18 | OPEN, 19 | CLOSING, 20 | CLOSED 21 | } 22 | 23 | interface ADNLClient { 24 | emit(event: 'connect'): boolean 25 | emit(event: 'ready'): boolean 26 | emit(event: 'close'): boolean 27 | emit(event: 'data', data: Buffer): boolean 28 | emit(event: 'error', error: Error): boolean 29 | 30 | on(event: 'connect', listener: () => void): this 31 | on(event: 'ready', listener: () => void): this 32 | on(event: 'close', listener: () => void): this 33 | on(event: 'data', listener: (data: Buffer) => void): this 34 | on(event: 'error', listener: (error: Error, close: boolean) => void): this 35 | 36 | once(event: 'connect', listener: () => void): this 37 | once(event: 'ready', listener: () => void): this 38 | once(event: 'close', listener: () => void): this 39 | once(event: 'data', listener: (data: Buffer) => void): this 40 | once(event: 'error', listener: (error: Error, close: boolean) => void): this 41 | } 42 | 43 | class ADNLClient extends EventEmitter { 44 | protected socket: any 45 | 46 | protected host: string 47 | 48 | protected port: number 49 | 50 | private buffer: Buffer 51 | 52 | private address: ADNLAddress 53 | 54 | private params: ADNLAESParams 55 | 56 | private keys: ADNLKeys 57 | 58 | private cipher: Cipher 59 | 60 | private decipher: Decipher 61 | 62 | private _state = ADNLClientState.CLOSED 63 | 64 | constructor (socket: any, url: string, peerPublicKey: Uint8Array | string) { 65 | super() 66 | 67 | try { 68 | const { hostname, port } = new URL(url) 69 | 70 | this.host = hostname 71 | this.port = parseInt(port, 10) 72 | this.address = new ADNLAddress(peerPublicKey) 73 | this.socket = socket 74 | } catch (err) { 75 | throw err 76 | } 77 | } 78 | 79 | protected get handshake (): Buffer { 80 | const key = Buffer.concat([ this.keys.shared.slice(0, 16), this.params.hash.slice(16, 32) ]) 81 | const nonce = Buffer.concat([ this.params.hash.slice(0, 4), this.keys.shared.slice(20, 32) ]) 82 | const cipher = createCipheriv('aes-256-ctr', key, nonce) 83 | const payload = Buffer.concat([ cipher.update(this.params.bytes), cipher.final() ]) 84 | const packet = Buffer.concat([ this.address.hash, this.keys.public, this.params.hash, payload ]) 85 | 86 | return packet 87 | } 88 | 89 | public get state (): ADNLClientState { 90 | return this._state 91 | } 92 | 93 | protected async onBeforeConnect (): Promise { 94 | if (this.state !== ADNLClientState.CLOSED) { 95 | return undefined 96 | } 97 | 98 | const keys = new ADNLKeys(this.address.publicKey) 99 | 100 | await keys.generate() 101 | 102 | this.keys = keys 103 | this.params = new ADNLAESParams() 104 | this.cipher = createCipheriv('aes-256-ctr', this.params.txKey, this.params.txNonce) 105 | this.decipher = createDecipheriv('aes-256-ctr', this.params.rxKey, this.params.rxNonce) 106 | this.buffer = Buffer.from([]) 107 | this._state = ADNLClientState.CONNECTING 108 | } 109 | 110 | protected onConnect () { 111 | this.emit('connect') 112 | } 113 | 114 | protected onReady (): void { 115 | this._state = ADNLClientState.OPEN 116 | this.emit('ready') 117 | } 118 | 119 | protected onClose (): void { 120 | this._state = ADNLClientState.CLOSED 121 | this.emit('close') 122 | } 123 | 124 | protected onData (data: Buffer): void { 125 | this.buffer = Buffer.concat([ this.buffer, this.decrypt(data) ]) 126 | 127 | while (this.buffer.byteLength >= PACKET_MIN_SIZE) { 128 | const packet = ADNLPacket.parse(this.buffer) 129 | 130 | if (packet === null) { 131 | break 132 | } 133 | 134 | this.buffer = this.buffer.slice(packet.length, this.buffer.byteLength) 135 | 136 | if (this.state === ADNLClientState.CONNECTING) { 137 | packet.payload.length !== 0 138 | ? this.onError(new Error('ADNLClient: Bad handshake.'), true) 139 | : this.onReady() 140 | 141 | break 142 | } 143 | 144 | this.emit('data', packet.payload) 145 | } 146 | } 147 | 148 | protected onError (error: Error, close = false): void { 149 | if (close) { 150 | this.socket.end() 151 | } 152 | 153 | this.emit('error', error) 154 | } 155 | 156 | protected onHandshake (): void { 157 | this.socket.write(this.handshake) 158 | } 159 | 160 | public write (data: Buffer): void { 161 | const packet = new ADNLPacket(data) 162 | const encrypted = this.encrypt(packet.data) 163 | 164 | this.socket.write(encrypted) 165 | } 166 | 167 | public async connect (): Promise { 168 | await this.onBeforeConnect() 169 | 170 | this.socket.connect(this.port, this.host) 171 | } 172 | 173 | public end (): void { 174 | if ( 175 | this.state === ADNLClientState.CLOSING 176 | || this.state === ADNLClientState.CLOSED 177 | ) { 178 | return undefined 179 | } 180 | 181 | this.socket.end() 182 | } 183 | 184 | protected encrypt (data: Buffer): Buffer { 185 | return Buffer.concat([ this.cipher.update(data) ]) 186 | } 187 | 188 | protected decrypt (data: Buffer): Buffer { 189 | return Buffer.concat([ this.decipher.update(data) ]) 190 | } 191 | } 192 | 193 | export { 194 | ADNLClient, 195 | ADNLClientState 196 | } 197 | --------------------------------------------------------------------------------