2021-04-08 12:19:38 +03:00
|
|
|
import { BigInteger } from 'big-integer'
|
|
|
|
import { buffersEqual, randomBytes } from '../utils/buffer-utils'
|
|
|
|
import { ICryptoProvider } from '../utils/crypto'
|
|
|
|
import { tl } from '@mtcute/tl'
|
|
|
|
import { createAesIgeForMessage } from '../utils/crypto/mtproto'
|
|
|
|
import {
|
|
|
|
BinaryWriter,
|
|
|
|
SerializationCounter,
|
|
|
|
} from '../utils/binary/binary-writer'
|
|
|
|
import { BinaryReader } from '../utils/binary/binary-reader'
|
|
|
|
|
|
|
|
const debug = require('debug')('mtcute:sess')
|
|
|
|
|
|
|
|
export interface EncryptedMessage {
|
|
|
|
messageId: BigInteger
|
|
|
|
seqNo: number
|
|
|
|
content: tl.TlObject
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class encapsulating a single MTProto session.
|
|
|
|
* Provides means to en-/decrypt messages
|
|
|
|
*/
|
|
|
|
export class MtprotoSession {
|
|
|
|
readonly _crypto: ICryptoProvider
|
|
|
|
|
|
|
|
_sessionId = randomBytes(8)
|
|
|
|
|
|
|
|
_authKey?: Buffer
|
|
|
|
_authKeyId?: Buffer
|
|
|
|
_authKeyClientSalt?: Buffer
|
|
|
|
_authKeyServerSalt?: Buffer
|
|
|
|
|
|
|
|
// default salt: [0x00]*8
|
|
|
|
serverSalt: Buffer = Buffer.alloc(8)
|
|
|
|
|
|
|
|
constructor(crypto: ICryptoProvider) {
|
|
|
|
this._crypto = crypto
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Whether session contains authKey */
|
|
|
|
get authorized(): boolean {
|
|
|
|
return this._authKey !== undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Setup keys based on authKey */
|
|
|
|
async setupKeys(authKey: Buffer): Promise<void> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Reset session by removing authKey and values derived from it */
|
|
|
|
reset(): void {
|
|
|
|
this._authKey = undefined
|
|
|
|
this._authKeyClientSalt = undefined
|
|
|
|
this._authKeyServerSalt = undefined
|
|
|
|
this._authKeyId = undefined
|
|
|
|
this._sessionId = randomBytes(8)
|
|
|
|
// no need to reset server salt
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Encrypt a single MTProto message using session's keys */
|
|
|
|
async encryptMessage(
|
|
|
|
message: tl.TlObject | Buffer,
|
|
|
|
messageId: BigInteger,
|
|
|
|
seqNo: number
|
|
|
|
): Promise<Buffer> {
|
|
|
|
if (!this._authKey) throw new Error('Keys are not set up!')
|
|
|
|
|
|
|
|
const length =
|
2021-05-23 13:42:38 +03:00
|
|
|
Buffer.isBuffer(message)
|
2021-04-08 12:19:38 +03:00
|
|
|
? message.length
|
2021-05-23 13:42:38 +03:00
|
|
|
: SerializationCounter.countNeededBytes(message)
|
2021-04-08 12:19:38 +03:00
|
|
|
let padding =
|
|
|
|
(32 /* header size */ + length + 12) /* min padding */ % 16
|
|
|
|
padding = 12 + (padding ? 16 - padding : 0)
|
|
|
|
const encryptedWriter = BinaryWriter.alloc(32 + length + padding)
|
|
|
|
|
|
|
|
encryptedWriter.raw(this.serverSalt!)
|
|
|
|
encryptedWriter.raw(this._sessionId)
|
|
|
|
encryptedWriter.long(messageId)
|
|
|
|
encryptedWriter.int32(seqNo)
|
|
|
|
encryptedWriter.uint32(length)
|
2021-05-23 13:42:38 +03:00
|
|
|
if (Buffer.isBuffer(message)) encryptedWriter.raw(message)
|
2021-05-15 21:17:49 +03:00
|
|
|
else encryptedWriter.object(message as tl.TlObject)
|
2021-04-08 12:19:38 +03:00
|
|
|
encryptedWriter.raw(randomBytes(padding))
|
|
|
|
|
|
|
|
const innerData = encryptedWriter.result()
|
|
|
|
const messageKey = (
|
|
|
|
await this._crypto.sha256(
|
|
|
|
Buffer.concat([this._authKeyClientSalt!, innerData])
|
|
|
|
)
|
|
|
|
).slice(8, 24)
|
|
|
|
const ige = await createAesIgeForMessage(
|
|
|
|
this._crypto,
|
|
|
|
this._authKey,
|
|
|
|
messageKey,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
const encryptedData = await ige.encrypt(innerData)
|
|
|
|
|
|
|
|
return Buffer.concat([this._authKeyId!, messageKey, encryptedData])
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Decrypt a single MTProto message using session's keys */
|
|
|
|
async decryptMessage(data: Buffer): Promise<EncryptedMessage | null> {
|
|
|
|
if (!this._authKey) throw new Error('Keys are not set up!')
|
|
|
|
|
|
|
|
const reader = new BinaryReader(data)
|
|
|
|
|
|
|
|
const authKeyId = reader.raw(8)
|
|
|
|
const messageKey = reader.int128()
|
2021-05-24 20:29:18 +03:00
|
|
|
let encryptedData = reader.raw()
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
if (!buffersEqual(authKeyId, this._authKeyId!)) {
|
|
|
|
debug(
|
|
|
|
'[%h] warn: received message with unknown authKey = %h (expected %h)',
|
|
|
|
this._sessionId,
|
|
|
|
authKeyId,
|
|
|
|
this._authKeyId
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
if (!buffersEqual(messageKey, expectedMessageKey)) {
|
|
|
|
debug(
|
|
|
|
'[%h] warn: received message with invalid messageKey = %h (expected %h)',
|
|
|
|
this._sessionId,
|
|
|
|
messageKey,
|
|
|
|
expectedMessageKey
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const innerReader = new BinaryReader(innerData)
|
|
|
|
innerReader.seek(8) // skip salt
|
|
|
|
const sessionId = innerReader.raw(8)
|
|
|
|
const messageId = innerReader.long(true)
|
|
|
|
|
|
|
|
if (!buffersEqual(sessionId, this._sessionId)) {
|
|
|
|
debug(
|
|
|
|
'warn: ignoring message with invalid sessionId = %h',
|
|
|
|
sessionId
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const seqNo = innerReader.uint32()
|
|
|
|
const length = innerReader.uint32()
|
|
|
|
|
|
|
|
if (length > innerData.length - 32 /* header size */) {
|
|
|
|
debug(
|
|
|
|
'warn: ignoring message with invalid length: %d > %d',
|
|
|
|
length,
|
|
|
|
innerData.length - 32
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
if (length % 4 !== 0) {
|
|
|
|
debug(
|
|
|
|
'warn: ignoring message with invalid length: %d is not a multiple of 4',
|
|
|
|
length
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const content = innerReader.object()
|
|
|
|
return {
|
|
|
|
messageId,
|
|
|
|
seqNo,
|
|
|
|
content,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|