owpengram-server/internal/mtprotoedge/decrypt.go

93 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mtprotoedge
import (
"crypto/aes"
"encoding/binary"
"errors"
"fmt"
"github.com/gotd/ige"
"github.com/gotd/td/bin"
"github.com/gotd/td/crypto"
)
// clientFrame 是解密后的单帧客户端消息视图。data/plaintext 引用调用方持有的复用明文
// 缓冲仅在解密下一帧前有效需要跨帧保留的字节必须拷贝dispatch 对 RPC body 已
// b.Copy()container/gzip 均在当前帧内同步消费)。
type clientFrame struct {
salt int64
sessionID int64
messageID int64
seqNo int32
// data 是去掉 32 字节头与 padding 后的消息体。
data []byte
// plaintext 是完整明文(头 + 数据 + paddingquick ack token 直接对它做 SHA256
// 免去 gotd 路径上「为算 token 把解密结果整帧重编码一遍」的拷贝。
plaintext []byte
}
// encryptedFrameHeaderLen 是 MTProto 2.0 明文头长度salt(8)+session_id(8)+msg_id(8)+seq_no(4)+len(4)。
const encryptedFrameHeaderLen = 32
// decryptClientFrame 把一帧客户端加密消息解密进 plain 复用缓冲telesrv-owned
// 与出站 encryptOutboundFrame 对称)。校验集合与 gotd Cipher.Decrypt 逐条一致:
// auth_key_id 匹配、密文 16 字节块对齐、msg_keySHA256client side x=0、明文头
// 长度、message_data_len 非负 / 4 字节对齐 / 不越界、padding ≤ 1024。区别只在
// 明文缓冲复用与免结构体堆分配——gotd DecryptFromBuffer 每帧 make 整帧明文
// (上传帧可达 512KiB+),是入站 Dispatch 链路最大的稳态分配点。
func decryptClientFrame(key crypto.AuthKey, b *bin.Buffer, plain *bin.Buffer) (clientFrame, error) {
buf := b.Buf
if len(buf) < 24 {
return clientFrame{}, errors.New("encrypted message is too short")
}
var authKeyID [8]byte
copy(authKeyID[:], buf[:8])
if authKeyID != key.ID {
return clientFrame{}, errors.New("unknown auth key id")
}
var msgKey bin.Int128
copy(msgKey[:], buf[8:24])
encrypted := buf[24:]
if len(encrypted) == 0 || len(encrypted)%16 != 0 {
return clientFrame{}, errors.New("invalid encrypted data padding")
}
aesKey, iv := crypto.Keys(key.Value, msgKey, crypto.Client)
aesBlock, err := aes.NewCipher(aesKey[:])
if err != nil {
return clientFrame{}, err
}
ensureBinBufferLen(plain, len(encrypted))
ige.DecryptBlocks(aesBlock, iv[:], plain.Buf, encrypted)
plaintext := plain.Buf
if crypto.MessageKey(key.Value, plaintext, crypto.Client) != msgKey {
return clientFrame{}, errors.New("msg_key is invalid")
}
if len(plaintext) < encryptedFrameHeaderLen {
return clientFrame{}, errors.New("message data is too short")
}
dataLen := int(int32(binary.LittleEndian.Uint32(plaintext[28:32])))
withPadding := plaintext[encryptedFrameHeaderLen:]
switch {
case dataLen < 0:
return clientFrame{}, fmt.Errorf("message length is invalid: %d less than zero", dataLen)
case dataLen%4 != 0:
return clientFrame{}, fmt.Errorf("message length is invalid: %d is not divisible by 4", dataLen)
case dataLen > len(withPadding):
return clientFrame{}, fmt.Errorf("message length %d is bigger than data length %d", dataLen, len(withPadding))
case len(withPadding)-dataLen > 1024:
return clientFrame{}, fmt.Errorf("padding %d of message is too big", len(withPadding)-dataLen)
}
return clientFrame{
salt: int64(binary.LittleEndian.Uint64(plaintext[0:8])),
sessionID: int64(binary.LittleEndian.Uint64(plaintext[8:16])),
messageID: int64(binary.LittleEndian.Uint64(plaintext[16:24])),
seqNo: int32(binary.LittleEndian.Uint32(plaintext[24:28])),
data: withPadding[:dataLen],
plaintext: plaintext,
}, nil
}