MTProtoState.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const struct = require('python-struct')
  2. const Helpers = require('../Helpers')
  3. const AES = require('../crypto/AES')
  4. const BinaryReader = require('../extensions/BinaryReader')
  5. const GZIPPacked = require('../tl/core/GZIPPacked')
  6. const { TLMessage } = require('../tl/core')
  7. const { SecurityError, InvalidBufferError } = require('../errors/Common')
  8. const { InvokeAfterMsgRequest } = require('../tl/functions')
  9. class MTProtoState {
  10. /**
  11. *
  12. `telethon.network.mtprotosender.MTProtoSender` needs to hold a state
  13. in order to be able to encrypt and decrypt incoming/outgoing messages,
  14. as well as generating the message IDs. Instances of this class hold
  15. together all the required information.
  16. It doesn't make sense to use `telethon.sessions.abstract.Session` for
  17. the sender because the sender should *not* be concerned about storing
  18. this information to disk, as one may create as many senders as they
  19. desire to any other data center, or some CDN. Using the same session
  20. for all these is not a good idea as each need their own authkey, and
  21. the concept of "copying" sessions with the unnecessary entities or
  22. updates state for these connections doesn't make sense.
  23. While it would be possible to have a `MTProtoPlainState` that does no
  24. encryption so that it was usable through the `MTProtoLayer` and thus
  25. avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more
  26. focused to efficiency and this state is also more advanced (since it
  27. supports gzipping and invoking after other message IDs). There are too
  28. many methods that would be needed to make it convenient to use for the
  29. authentication process, at which point the `MTProtoPlainSender` is better
  30. * @param authKey
  31. * @param loggers
  32. */
  33. constructor(authKey, loggers) {
  34. this.authKey = authKey
  35. this._log = loggers
  36. this.timeOffset = 0
  37. this.salt = 0
  38. this.id = this._sequence = this._lastMsgId = null
  39. this.reset()
  40. }
  41. /**
  42. * Resets the state
  43. */
  44. reset() {
  45. // Session IDs can be random on every connection
  46. this.id = Helpers.generateRandomLong(true)
  47. this._sequence = 0
  48. this._lastMsgId = 0
  49. }
  50. /**
  51. * Updates the message ID to a new one,
  52. * used when the time offset changed.
  53. * @param message
  54. */
  55. updateMessageId(message) {
  56. message.msgId = this._getNewMsgId()
  57. }
  58. /**
  59. * Calculate the key based on Telegram guidelines, specifying whether it's the client or not
  60. * @param authKey
  61. * @param msgKey
  62. * @param client
  63. * @returns {{iv: Buffer, key: Buffer}}
  64. */
  65. _calcKey(authKey, msgKey, client) {
  66. const x = client === true ? 0 : 8
  67. const sha256a = Helpers.sha256(Buffer.concat([msgKey, authKey.slice(x, x + 36)]))
  68. const sha256b = Helpers.sha256(Buffer.concat([authKey.slice(x + 40, x + 76), msgKey]))
  69. const key = Buffer.concat([sha256a.slice(0, 8), sha256b.slice(8, 24), sha256a.slice(24, 32)])
  70. const iv = Buffer.concat([sha256b.slice(0, 8), sha256a.slice(8, 24), sha256b.slice(24, 32)])
  71. return { key, iv }
  72. }
  73. /**
  74. * Writes a message containing the given data into buffer.
  75. * Returns the message id.
  76. * @param buffer
  77. * @param data
  78. * @param contentRelated
  79. * @param afterId
  80. */
  81. async writeDataAsMessage(buffer, data, contentRelated, afterId) {
  82. const msgId = this._getNewMsgId()
  83. const seqNo = this._getSeqNo(contentRelated)
  84. let body
  85. if (!afterId) {
  86. body = await GZIPPacked.gzipIfSmaller(contentRelated, data)
  87. } else {
  88. body = await GZIPPacked.gzipIfSmaller(contentRelated, new InvokeAfterMsgRequest(afterId, data).toBuffer())
  89. }
  90. buffer.write(struct.pack('<qii', msgId.toString(), seqNo, body.length))
  91. buffer.write(body)
  92. return msgId
  93. }
  94. /**
  95. * Encrypts the given message data using the current authorization key
  96. * following MTProto 2.0 guidelines core.telegram.org/mtproto/description.
  97. * @param data
  98. */
  99. encryptMessageData(data) {
  100. data = Buffer.concat([struct.pack('<qq', this.salt.toString(), this.id.toString()), data])
  101. const padding = Helpers.generateRandomBytes(Helpers.mod(-(data.length + 12), 16) + 12)
  102. // Being substr(what, offset, length); x = 0 for client
  103. // "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
  104. const msgKeyLarge = Helpers.sha256(Buffer.concat([this.authKey.key.slice(88, 88 + 32), data, padding]))
  105. // "msg_key = substr (msg_key_large, 8, 16)"
  106. const msgKey = msgKeyLarge.slice(8, 24)
  107. const { iv, key } = this._calcKey(this.authKey.key, msgKey, true)
  108. const keyId = Helpers.readBufferFromBigInt(this.authKey.keyId, 8)
  109. return Buffer.concat([keyId, msgKey, AES.encryptIge(Buffer.concat([data, padding]), key, iv)])
  110. }
  111. /**
  112. * Inverse of `encrypt_message_data` for incoming server messages.
  113. * @param body
  114. */
  115. async decryptMessageData(body) {
  116. if (body.length < 8) {
  117. throw new InvalidBufferError(body)
  118. }
  119. // TODO Check salt,sessionId, and sequenceNumber
  120. const keyId = Helpers.readBigIntFromBuffer(body.slice(0, 8))
  121. if (keyId !== this.authKey.keyId) {
  122. throw new SecurityError('Server replied with an invalid auth key')
  123. }
  124. const msgKey = body.slice(8, 24)
  125. const { iv, key } = this._calcKey(this.authKey.key, msgKey, false)
  126. body = AES.decryptIge(body.slice(24), key, iv)
  127. // https://core.telegram.org/mtproto/security_guidelines
  128. // Sections "checking sha256 hash" and "message length"
  129. const ourKey = Helpers.sha256(Buffer.concat([this.authKey.key.slice(96, 96 + 32), body]))
  130. if (!msgKey.equals(ourKey.slice(8, 24))) {
  131. throw new SecurityError('Received msg_key doesn\'t match with expected one')
  132. }
  133. const reader = new BinaryReader(body)
  134. reader.readLong() // removeSalt
  135. const serverId = reader.readLong()
  136. if (serverId !== this.id) {
  137. // throw new SecurityError('Server replied with a wrong session ID');
  138. }
  139. const remoteMsgId = reader.readLong()
  140. const remoteSequence = reader.readInt()
  141. reader.readInt() // msgLen for the inner object, padding ignored
  142. // We could read msg_len bytes and use those in a new reader to read
  143. // the next TLObject without including the padding, but since the
  144. // reader isn't used for anything else after this, it's unnecessary.
  145. const obj = await reader.tgReadObject()
  146. return new TLMessage(remoteMsgId, remoteSequence, obj)
  147. }
  148. /**
  149. * Generates a new unique message ID based on the current
  150. * time (in ms) since epoch, applying a known time offset.
  151. * @private
  152. */
  153. _getNewMsgId() {
  154. const now = new Date().getTime() / 1000 + this.timeOffset
  155. const nanoseconds = Math.floor((now - Math.floor(now)) * 1e9)
  156. let newMsgId = (BigInt(Math.floor(now)) << 32n) | (BigInt(nanoseconds) << 2n)
  157. if (this._lastMsgId >= newMsgId) {
  158. newMsgId = this._lastMsgId + 4n
  159. }
  160. this._lastMsgId = newMsgId
  161. return newMsgId
  162. }
  163. /**
  164. * Updates the time offset to the correct
  165. * one given a known valid message ID.
  166. * @param correctMsgId
  167. */
  168. updateTimeOffset(correctMsgId) {
  169. const bad = this._getNewMsgId()
  170. const old = this.timeOffset
  171. const now = Math.floor(new Date().getTime() / 1000)
  172. const correct = correctMsgId >> 32n
  173. this.timeOffset = correct - now
  174. if (this.timeOffset !== old) {
  175. this._lastMsgId = 0
  176. this._log.debug(
  177. `Updated time offset (old offset ${old}, bad ${bad}, good ${correctMsgId}, new ${this.timeOffset})`,
  178. )
  179. }
  180. return this.timeOffset
  181. }
  182. /**
  183. * Generates the next sequence number depending on whether
  184. * it should be for a content-related query or not.
  185. * @param contentRelated
  186. * @private
  187. */
  188. _getSeqNo(contentRelated) {
  189. if (contentRelated) {
  190. const result = this._sequence * 2 + 1
  191. this._sequence += 1
  192. return result
  193. } else {
  194. return this._sequence * 2
  195. }
  196. }
  197. }
  198. module.exports = MTProtoState