2022-06-30 16:32:56 +03:00
|
|
|
import Long from 'long'
|
2021-11-23 00:03:59 +03:00
|
|
|
import { mtp, tl } from '@mtcute/tl'
|
2022-06-30 16:32:56 +03:00
|
|
|
import {
|
|
|
|
TlBinaryReader,
|
|
|
|
TlBinaryWriter,
|
|
|
|
TlReaderMap,
|
|
|
|
TlSerializationCounter,
|
|
|
|
TlWriterMap,
|
|
|
|
} from '@mtcute/tl-runtime'
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
import { createAesIgeForMessage } from '../utils/crypto/mtproto'
|
2022-06-30 16:32:56 +03:00
|
|
|
import { buffersEqual, randomBytes } from '../utils/buffer-utils'
|
2021-11-23 00:03:59 +03:00
|
|
|
import { ICryptoProvider, Logger, getRandomInt, randomLong } from '../utils'
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class encapsulating a single MTProto session.
|
|
|
|
* Provides means to en-/decrypt messages
|
|
|
|
*/
|
|
|
|
export class MtprotoSession {
|
|
|
|
readonly _crypto: ICryptoProvider
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
_sessionId = randomLong()
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
_authKey?: Buffer
|
|
|
|
_authKeyId?: Buffer
|
|
|
|
_authKeyClientSalt?: Buffer
|
|
|
|
_authKeyServerSalt?: Buffer
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
_timeOffset = 0
|
|
|
|
_lastMessageId = Long.ZERO
|
|
|
|
_seqNo = 0
|
|
|
|
|
|
|
|
serverSalt = Long.ZERO
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
constructor(
|
|
|
|
crypto: ICryptoProvider,
|
|
|
|
readonly log: Logger,
|
|
|
|
readonly _readerMap: TlReaderMap,
|
|
|
|
readonly _writerMap: TlWriterMap
|
|
|
|
) {
|
2021-04-08 12:19:38 +03:00
|
|
|
this._crypto = crypto
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Whether session contains authKey */
|
|
|
|
get authorized(): boolean {
|
|
|
|
return this._authKey !== undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Setup keys based on authKey */
|
2021-11-23 00:03:59 +03:00
|
|
|
async setupKeys(authKey?: Buffer | null): Promise<void> {
|
|
|
|
if (authKey) {
|
|
|
|
this._authKey = authKey
|
|
|
|
this._authKeyClientSalt = authKey.slice(88, 120)
|
|
|
|
this._authKeyServerSalt = authKey.slice(96, 128)
|
|
|
|
this._authKeyId = (await this._crypto.sha1(this._authKey)).slice(-8)
|
|
|
|
} else {
|
|
|
|
this._authKey = undefined
|
|
|
|
this._authKeyClientSalt = undefined
|
|
|
|
this._authKeyServerSalt = undefined
|
|
|
|
this._authKeyId = undefined
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Reset session by removing authKey and values derived from it */
|
|
|
|
reset(): void {
|
2021-11-23 00:03:59 +03:00
|
|
|
this._lastMessageId = Long.ZERO
|
|
|
|
this._seqNo = 0
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
this._authKey = undefined
|
|
|
|
this._authKeyClientSalt = undefined
|
|
|
|
this._authKeyServerSalt = undefined
|
|
|
|
this._authKeyId = undefined
|
2021-11-23 00:03:59 +03:00
|
|
|
this._sessionId = randomLong()
|
2021-04-08 12:19:38 +03:00
|
|
|
// no need to reset server salt
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
changeSessionId(): void {
|
|
|
|
this._sessionId = randomLong()
|
|
|
|
this._seqNo = 0
|
|
|
|
}
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/** Encrypt a single MTProto message using session's keys */
|
2021-11-23 00:03:59 +03:00
|
|
|
async encryptMessage(message: Buffer): Promise<Buffer> {
|
2021-04-08 12:19:38 +03:00
|
|
|
if (!this._authKey) throw new Error('Keys are not set up!')
|
|
|
|
|
|
|
|
let padding =
|
2021-11-23 00:03:59 +03:00
|
|
|
(16 /* header size */ + message.length + 12) /* min padding */ % 16
|
2021-04-08 12:19:38 +03:00
|
|
|
padding = 12 + (padding ? 16 - padding : 0)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
const buf = Buffer.alloc(16 + message.length + padding)
|
|
|
|
|
|
|
|
buf.writeInt32LE(this.serverSalt!.low)
|
|
|
|
buf.writeInt32LE(this.serverSalt!.high, 4)
|
|
|
|
buf.writeInt32LE(this._sessionId.low, 8)
|
|
|
|
buf.writeInt32LE(this._sessionId.high, 12)
|
|
|
|
message.copy(buf, 16)
|
|
|
|
randomBytes(padding).copy(buf, 16 + message.length)
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
const messageKey = (
|
|
|
|
await this._crypto.sha256(
|
2021-11-23 00:03:59 +03:00
|
|
|
Buffer.concat([this._authKeyClientSalt!, buf])
|
2021-04-08 12:19:38 +03:00
|
|
|
)
|
|
|
|
).slice(8, 24)
|
|
|
|
const ige = await createAesIgeForMessage(
|
|
|
|
this._crypto,
|
|
|
|
this._authKey,
|
|
|
|
messageKey,
|
|
|
|
true
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
const encryptedData = await ige.encrypt(buf)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
return Buffer.concat([this._authKeyId!, messageKey, encryptedData])
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Decrypt a single MTProto message using session's keys */
|
2021-11-23 00:03:59 +03:00
|
|
|
async decryptMessage(
|
|
|
|
data: Buffer,
|
2022-06-30 16:32:56 +03:00
|
|
|
callback: (msgId: tl.Long, seqNo: number, data: TlBinaryReader) => void
|
2021-11-23 00:03:59 +03:00
|
|
|
): Promise<void> {
|
2021-04-08 12:19:38 +03:00
|
|
|
if (!this._authKey) throw new Error('Keys are not set up!')
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const authKeyId = data.slice(0, 8)
|
|
|
|
const messageKey = data.slice(8, 24)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
let encryptedData = data.slice(24)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
if (!buffersEqual(authKeyId, this._authKeyId!)) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
2021-04-08 12:19:38 +03:00
|
|
|
'[%h] warn: received message with unknown authKey = %h (expected %h)',
|
|
|
|
this._sessionId,
|
|
|
|
authKeyId,
|
|
|
|
this._authKeyId
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-05-24 20:29:18 +03:00
|
|
|
const padSize = encryptedData.length % 16
|
|
|
|
if (padSize !== 0) {
|
|
|
|
// data came from a codec that uses non-16-based padding.
|
|
|
|
// it is safe to drop those padding bytes
|
|
|
|
encryptedData = encryptedData.slice(0, -padSize)
|
|
|
|
}
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
const ige = await createAesIgeForMessage(
|
|
|
|
this._crypto,
|
|
|
|
this._authKey!,
|
|
|
|
messageKey,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
const innerData = await ige.decrypt(encryptedData)
|
|
|
|
|
|
|
|
const expectedMessageKey = (
|
|
|
|
await this._crypto.sha256(
|
|
|
|
Buffer.concat([this._authKeyServerSalt!, innerData])
|
|
|
|
)
|
|
|
|
).slice(8, 24)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
if (!buffersEqual(messageKey, expectedMessageKey)) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
|
|
|
'[%h] received message with invalid messageKey = %h (expected %h)',
|
2021-04-08 12:19:38 +03:00
|
|
|
this._sessionId,
|
|
|
|
messageKey,
|
|
|
|
expectedMessageKey
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const innerReader = new TlBinaryReader(this._readerMap, innerData)
|
2021-04-08 12:19:38 +03:00
|
|
|
innerReader.seek(8) // skip salt
|
2021-11-23 00:03:59 +03:00
|
|
|
const sessionId = innerReader.long()
|
2021-04-08 12:19:38 +03:00
|
|
|
const messageId = innerReader.long(true)
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (sessionId.neq(this._sessionId)) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
|
|
|
'ignoring message with invalid sessionId = %h',
|
2021-04-08 12:19:38 +03:00
|
|
|
sessionId
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const seqNo = innerReader.uint()
|
|
|
|
const length = innerReader.uint()
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
if (length > innerData.length - 32 /* header size */) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
|
|
|
'ignoring message with invalid length: %d > %d',
|
2021-04-08 12:19:38 +03:00
|
|
|
length,
|
|
|
|
innerData.length - 32
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (length % 4 !== 0) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
|
|
|
'ignoring message with invalid length: %d is not a multiple of 4',
|
2021-04-08 12:19:38 +03:00
|
|
|
length
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const paddingSize = innerData.length - length - 32 // header size
|
2021-06-15 03:11:52 +03:00
|
|
|
|
|
|
|
if (paddingSize < 12 || paddingSize > 1024) {
|
2021-08-14 12:57:26 +03:00
|
|
|
this.log.warn(
|
|
|
|
'ignoring message with invalid padding size: %d',
|
2021-06-15 03:11:52 +03:00
|
|
|
paddingSize
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
2021-06-15 03:11:52 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
callback(messageId, seqNo, innerReader)
|
|
|
|
}
|
|
|
|
|
|
|
|
getMessageId(): Long {
|
|
|
|
const timeTicks = Date.now()
|
|
|
|
const timeSec = Math.floor(timeTicks / 1000) + this._timeOffset
|
|
|
|
const timeMSec = timeTicks % 1000
|
|
|
|
const random = getRandomInt(0xffff)
|
|
|
|
|
|
|
|
let messageId = new Long((timeMSec << 21) | (random << 3) | 4, timeSec)
|
|
|
|
|
|
|
|
if (this._lastMessageId.gt(messageId)) {
|
|
|
|
messageId = this._lastMessageId.add(4)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._lastMessageId = messageId
|
|
|
|
|
|
|
|
return messageId
|
|
|
|
}
|
|
|
|
|
|
|
|
getSeqNo(isContentRelated = true): number {
|
|
|
|
let seqNo = this._seqNo * 2
|
|
|
|
|
|
|
|
if (isContentRelated) {
|
|
|
|
seqNo += 1
|
|
|
|
this._seqNo += 1
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
return seqNo
|
|
|
|
}
|
|
|
|
|
|
|
|
writeMessage(
|
|
|
|
writer: TlBinaryWriter,
|
|
|
|
content: tl.TlObject | mtp.TlObject | Buffer,
|
|
|
|
isContentRelated = true
|
|
|
|
): Long {
|
|
|
|
const messageId = this.getMessageId()
|
|
|
|
const seqNo = this.getSeqNo(isContentRelated)
|
|
|
|
|
|
|
|
const length = Buffer.isBuffer(content)
|
|
|
|
? content.length
|
2022-06-30 16:32:56 +03:00
|
|
|
: TlSerializationCounter.countNeededBytes(
|
|
|
|
writer.objectMap!,
|
|
|
|
content
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
writer.long(messageId)
|
|
|
|
writer.int(seqNo)
|
|
|
|
writer.uint(length)
|
|
|
|
if (Buffer.isBuffer(content)) writer.raw(content)
|
|
|
|
else writer.object(content as tl.TlObject)
|
|
|
|
|
|
|
|
return messageId
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
}
|