mtcute/packages/core/src/network/mtproto-session.ts

420 lines
11 KiB
TypeScript
Raw Normal View History

import Long from 'long'
import { mtp, tl } from '@mtcute/tl'
import {
TlBinaryReader,
TlBinaryWriter,
TlReaderMap,
TlSerializationCounter,
TlWriterMap,
} from '@mtcute/tl-runtime'
import { getRandomInt, ICryptoProvider, Logger, randomLong } from '../utils'
import { buffersEqual, randomBytes } from '../utils/buffer-utils'
import {
ICryptoProvider,
Logger,
getRandomInt,
randomLong,
ControllablePromise,
LruSet,
Deque,
SortedArray,
LongMap,
} from '../utils'
import { createAesIgeForMessage } from '../utils/crypto/mtproto'
2021-04-08 12:19:38 +03:00
export interface PendingRpc {
method: string
data: Buffer
promise: ControllablePromise
stack?: string
gzipOverhead?: number
sent?: boolean
msgId?: Long
seqNo?: number
containerId?: Long
acked?: boolean
initConn?: boolean
getState?: number
cancelled?: boolean
timeout?: number
}
export type PendingMessage =
| {
_: 'rpc'
rpc: PendingRpc
}
| {
_: 'container'
msgIds: Long[]
}
| {
_: 'state'
msgIds: Long[]
containerId: Long
}
| {
_: 'resend'
msgIds: Long[]
containerId: Long
}
| {
_: 'ping'
pingId: Long
containerId: Long
}
| {
_: 'destroy_session'
sessionId: Long
containerId: Long
}
| {
_: 'cancel'
msgId: Long
containerId: Long
}
| {
_: 'future_salts'
containerId: Long
}
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
_sessionId = randomLong()
2021-04-08 12:19:38 +03:00
_authKey?: Buffer
_authKeyId?: Buffer
_authKeyClientSalt?: Buffer
_authKeyServerSalt?: Buffer
_timeOffset = 0
_lastMessageId = Long.ZERO
_seqNo = 0
serverSalt = Long.ZERO
2021-04-08 12:19:38 +03:00
/// state ///
// recent msg ids
recentOutgoingMsgIds = new LruSet<Long>(1000, false, true)
recentIncomingMsgIds = new LruSet<Long>(1000, false, true)
// queues
queuedRpc = new Deque<PendingRpc>()
queuedAcks: Long[] = []
queuedStateReq: Long[] = []
queuedResendReq: Long[] = []
queuedCancelReq: Long[] = []
getStateSchedule = new SortedArray<PendingRpc>(
[],
(a, b) => a.getState! - b.getState!
)
// requests info
pendingMessages = new LongMap<PendingMessage>()
initConnectionCalled = false
constructor(
crypto: ICryptoProvider,
readonly log: Logger,
readonly _readerMap: TlReaderMap,
readonly _writerMap: TlWriterMap,
) {
2021-04-08 12:19:38 +03:00
this._crypto = crypto
this.log.prefix = `[SESSION ${this._sessionId.toString(16)}] `
2021-04-08 12:19:38 +03:00
}
/** Whether session contains authKey */
get authorized(): boolean {
return this._authKey !== undefined
}
/** Setup keys based on authKey */
async setupKeys(authKey?: Buffer | null): Promise<void> {
if (authKey) {
this.log.debug('setting up keys')
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.log.debug('resetting keys')
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,
* as well as resetting session state
*/
2021-04-08 12:19:38 +03:00
reset(): void {
this._authKey = undefined
this._authKeyClientSalt = undefined
this._authKeyServerSalt = undefined
this._authKeyId = undefined
this.resetState()
2021-04-08 12:19:38 +03:00
}
/**
* Reset session state and generate a new session ID.
*
* By default, also cancels any pending RPC requests.
* If `keepPending` is set to `true`, pending requests will be kept
*/
resetState(keepPending = false): void {
this._lastMessageId = Long.ZERO
this._seqNo = 0
this._sessionId = randomLong()
this.log.debug('session reset, new sid = %l', this._sessionId)
this.log.prefix = `[SESSION ${this._sessionId.toString(16)}] `
// reset session state
if (!keepPending) {
for (const info of this.pendingMessages.values()) {
if (info._ === 'rpc') {
info.rpc.promise.reject(new Error('Session is reset'))
}
}
this.pendingMessages.clear()
}
this.recentOutgoingMsgIds.clear()
this.recentIncomingMsgIds.clear()
if (!keepPending) {
while (this.queuedRpc.length) {
const rpc = this.queuedRpc.popFront()!
if (rpc.sent === false) {
rpc.promise.reject(new Error('Session is reset'))
}
}
}
this.queuedAcks.length = 0
this.queuedStateReq.length = 0
this.queuedResendReq.length = 0
this.getStateSchedule.clear()
}
enqueueRpc(rpc: PendingRpc, force?: boolean): boolean {
// already queued or cancelled
if ((!force && !rpc.sent) || rpc.cancelled) return false
rpc.sent = false
rpc.containerId = undefined
this.log.debug(
'enqueued %s for sending (msg_id = %s)',
rpc.method,
rpc.msgId || 'n/a'
)
this.queuedRpc.pushBack(rpc)
return true
}
2021-04-08 12:19:38 +03:00
/** Encrypt a single MTProto message using session's keys */
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 =
(16 /* header size */ + message.length + 12) /* min padding */ % 16
2021-04-08 12:19:38 +03:00
padding = 12 + (padding ? 16 - padding : 0)
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(
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-04-08 12:19:38 +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 */
async decryptMessage(
data: Buffer,
callback: (msgId: tl.Long, seqNo: number, data: TlBinaryReader) => void,
): Promise<void> {
2021-04-08 12:19:38 +03:00
if (!this._authKey) throw new Error('Keys are not set up!')
const authKeyId = data.slice(0, 8)
const messageKey = data.slice(8, 24)
2021-04-08 12:19:38 +03:00
let encryptedData = data.slice(24)
2021-04-08 12:19:38 +03:00
if (!buffersEqual(authKeyId, this._authKeyId!)) {
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-04-08 12:19:38 +03:00
)
return
2021-04-08 12:19:38 +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,
2021-04-08 12:19:38 +03:00
)
const innerData = await ige.decrypt(encryptedData)
const expectedMessageKey = (
await this._crypto.sha256(
Buffer.concat([this._authKeyServerSalt!, innerData]),
2021-04-08 12:19:38 +03:00
)
).slice(8, 24)
2021-04-08 12:19:38 +03:00
if (!buffersEqual(messageKey, expectedMessageKey)) {
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-04-08 12:19:38 +03:00
)
return
2021-04-08 12:19:38 +03:00
}
const innerReader = new TlBinaryReader(this._readerMap, innerData)
2021-04-08 12:19:38 +03:00
innerReader.seek(8) // skip salt
const sessionId = innerReader.long()
2021-04-08 12:19:38 +03:00
const messageId = innerReader.long(true)
if (sessionId.neq(this._sessionId)) {
this.log.warn(
'ignoring message with invalid sessionId = %h',
sessionId,
2021-04-08 12:19:38 +03:00
)
return
2021-04-08 12:19:38 +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 */) {
this.log.warn(
'ignoring message with invalid length: %d > %d',
2021-04-08 12:19:38 +03:00
length,
innerData.length - 32,
2021-04-08 12:19:38 +03:00
)
return
2021-04-08 12:19:38 +03:00
}
if (length % 4 !== 0) {
this.log.warn(
'ignoring message with invalid length: %d is not a multiple of 4',
length,
2021-04-08 12:19:38 +03:00
)
return
2021-04-08 12:19:38 +03:00
}
const paddingSize = innerData.length - length - 32 // header size
2021-06-15 03:11:52 +03:00
if (paddingSize < 12 || paddingSize > 1024) {
this.log.warn(
'ignoring message with invalid padding size: %d',
paddingSize,
2021-06-15 03:11:52 +03:00
)
return
2021-06-15 03:11:52 +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
}
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 :
TlSerializationCounter.countNeededBytes(
writer.objectMap!,
content,
)
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
}
}