479 lines
17 KiB
Go
479 lines
17 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha1"
|
||
"encoding/binary"
|
||
"math/big"
|
||
"testing"
|
||
|
||
"github.com/iamxvbaba/td/bin"
|
||
"github.com/iamxvbaba/td/clock"
|
||
"github.com/iamxvbaba/td/tg"
|
||
"github.com/iamxvbaba/td/tlprofile"
|
||
"go.uber.org/zap/zaptest"
|
||
|
||
appphone "telesrv/internal/app/phone"
|
||
appsecret "telesrv/internal/app/secretchat"
|
||
appupdates "telesrv/internal/app/updates"
|
||
appusers "telesrv/internal/app/users"
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
func dhParam(lead byte) []byte {
|
||
b := make([]byte, 256)
|
||
for i := range b {
|
||
b[i] = 0x42
|
||
}
|
||
b[0] = lead
|
||
return b
|
||
}
|
||
|
||
type encryptedFixture struct {
|
||
ctx context.Context
|
||
router *Router
|
||
sessions *phoneCaptureSessions
|
||
store *memory.SecretChatStore
|
||
queue *memory.EncryptedQueueStore
|
||
admin domain.User
|
||
participant domain.User
|
||
}
|
||
|
||
const (
|
||
encAdminSession = int64(301)
|
||
encPartSession = int64(302)
|
||
encPartOtherSession = int64(303)
|
||
)
|
||
|
||
var (
|
||
encAdminAuthKey = [8]byte{1, 0, 0, 0, 0, 0, 0, 0}
|
||
encPartAuthKey = [8]byte{2, 0, 0, 0, 0, 0, 0, 0}
|
||
encPartOtherAuthKey = [8]byte{3, 0, 0, 0, 0, 0, 0, 0}
|
||
)
|
||
|
||
func newEncryptedFixture(t *testing.T) *encryptedFixture {
|
||
t.Helper()
|
||
ctx := context.Background()
|
||
userStore := memory.NewUserStore()
|
||
sessions := &phoneCaptureSessions{}
|
||
secretStore := memory.NewSecretChatStore()
|
||
queueStore := memory.NewEncryptedQueueStore()
|
||
router := New(Config{}, Deps{
|
||
Users: appusers.NewService(userStore),
|
||
SecretChats: appsecret.NewService(secretStore, queueStore),
|
||
Updates: appupdates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore()),
|
||
Files: &fakeFiles{},
|
||
Sessions: sessions,
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
f := &encryptedFixture{ctx: ctx, router: router, sessions: sessions, store: secretStore, queue: queueStore}
|
||
mk := func(hash int64, phone, name string) domain.User {
|
||
u, err := userStore.Create(ctx, domain.User{AccessHash: hash, Phone: phone, FirstName: name})
|
||
if err != nil {
|
||
t.Fatalf("create user %s: %v", name, err)
|
||
}
|
||
return u
|
||
}
|
||
f.admin = mk(5001, "13800000011", "Admin")
|
||
f.participant = mk(5002, "13800000012", "Participant")
|
||
return f
|
||
}
|
||
|
||
func (f *encryptedFixture) adminCtx() context.Context {
|
||
return WithAuthKeyID(WithSessionID(WithUserID(f.ctx, f.admin.ID), encAdminSession), encAdminAuthKey)
|
||
}
|
||
|
||
func (f *encryptedFixture) participantCtx() context.Context {
|
||
return WithAuthKeyID(WithSessionID(WithUserID(f.ctx, f.participant.ID), encPartSession), encPartAuthKey)
|
||
}
|
||
|
||
func (f *encryptedFixture) participantOtherCtx() context.Context {
|
||
return WithAuthKeyID(WithSessionID(WithUserID(f.ctx, f.participant.ID), encPartOtherSession), encPartOtherAuthKey)
|
||
}
|
||
|
||
// encChatPayload 从捕获的推送里取出 updateEncryption 载荷。
|
||
func encChatPayload(t *testing.T, rec phonePushRecord) tg.EncryptedChatClass {
|
||
t.Helper()
|
||
updates, ok := rec.msg.(*tg.Updates)
|
||
if !ok || len(updates.Updates) != 1 {
|
||
t.Fatalf("pushed msg = %T, want single-update tg.Updates", rec.msg)
|
||
}
|
||
upd, ok := updates.Updates[0].(*tg.UpdateEncryption)
|
||
if !ok {
|
||
t.Fatalf("pushed update = %T, want UpdateEncryption", updates.Updates[0])
|
||
}
|
||
return upd.Chat
|
||
}
|
||
|
||
func secretChatDHFixture(t *testing.T, wantNegativeFingerprint bool) (ga, gb []byte, fingerprint int64) {
|
||
t.Helper()
|
||
prime := new(big.Int).SetBytes(appphone.DHPrime())
|
||
generator := big.NewInt(int64(appphone.DHG))
|
||
privateA := new(big.Int).SetBytes(make([]byte, 256))
|
||
privateA.SetBit(privateA, 2046, 1)
|
||
privateA.Add(privateA, big.NewInt(0x12345))
|
||
gaInt := new(big.Int).Exp(generator, privateA, prime)
|
||
ga = gaInt.Bytes()
|
||
|
||
for n := int64(1); n < 128; n++ {
|
||
privateB := new(big.Int).SetBit(new(big.Int), 2045, 1)
|
||
privateB.Add(privateB, big.NewInt(0x54321+n))
|
||
gbInt := new(big.Int).Exp(generator, privateB, prime)
|
||
sharedA := new(big.Int).Exp(gbInt, privateA, prime)
|
||
sharedB := new(big.Int).Exp(gaInt, privateB, prime)
|
||
if sharedA.Cmp(sharedB) != 0 {
|
||
t.Fatal("DH fixture derived different shared keys")
|
||
}
|
||
key := make([]byte, 256)
|
||
sharedBytes := sharedA.Bytes()
|
||
copy(key[len(key)-len(sharedBytes):], sharedBytes)
|
||
digest := sha1.Sum(key)
|
||
fingerprint = int64(binary.LittleEndian.Uint64(digest[12:20]))
|
||
if (fingerprint < 0) == wantNegativeFingerprint {
|
||
return ga, gbInt.Bytes(), fingerprint
|
||
}
|
||
}
|
||
t.Fatalf("could not generate DH fixture with negative=%v fingerprint", wantNegativeFingerprint)
|
||
return nil, nil, 0
|
||
}
|
||
|
||
func TestEncryptedChatRealDHHandshakeAcrossExactLayers(t *testing.T) {
|
||
for _, negative := range []bool{false, true} {
|
||
name := "positive_fingerprint"
|
||
if negative {
|
||
name = "negative_fingerprint"
|
||
}
|
||
t.Run(name, func(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
ga, gb, fingerprint := secretChatDHFixture(t, negative)
|
||
waitingClass, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: 909,
|
||
GA: ga,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("requestEncryption: %v", err)
|
||
}
|
||
waiting := waitingClass.(*tg.EncryptedChatWaiting)
|
||
if waiting.ID != 909 {
|
||
t.Fatalf("waiting id = %d, want request random_id 909", waiting.ID)
|
||
}
|
||
chat, ok, err := f.store.GetSecretChat(f.ctx, waiting.ID)
|
||
if err != nil || !ok {
|
||
t.Fatalf("stored requested chat: ok=%v err=%v", ok, err)
|
||
}
|
||
|
||
f.sessions.reset()
|
||
if _, err := f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
|
||
Peer: tg.InputEncryptedChat{ChatID: chat.ID, AccessHash: chat.ParticipantAccessHash},
|
||
GB: gb,
|
||
KeyFingerprint: fingerprint,
|
||
}); err != nil {
|
||
t.Fatalf("acceptEncryption: %v", err)
|
||
}
|
||
|
||
var adminUpdates *tg.Updates
|
||
for _, rec := range f.sessions.records() {
|
||
if rec.userID == f.admin.ID {
|
||
adminUpdates = rec.msg.(*tg.Updates)
|
||
break
|
||
}
|
||
}
|
||
if adminUpdates == nil {
|
||
t.Fatal("missing accepted update for the initiating device")
|
||
}
|
||
|
||
for _, profile := range []tlprofile.Profile{
|
||
tlprofile.Profile225,
|
||
tlprofile.Profile226,
|
||
tlprofile.Profile227,
|
||
tlprofile.Profile228,
|
||
} {
|
||
var body bin.Buffer
|
||
if err := tlprofile.EncodeObject(profile, adminUpdates, &body); err != nil {
|
||
t.Fatalf("encode accepted update for profile %d: %v", profile, err)
|
||
}
|
||
decoded, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: body.Buf}, tlprofile.Limits{})
|
||
if err != nil {
|
||
t.Fatalf("decode accepted update for profile %d: %v", profile, err)
|
||
}
|
||
updates, ok := decoded.(*tg.Updates)
|
||
if !ok || len(updates.Updates) != 1 {
|
||
t.Fatalf("profile %d decoded update = %T", profile, decoded)
|
||
}
|
||
encUpdate, ok := updates.Updates[0].(*tg.UpdateEncryption)
|
||
if !ok {
|
||
t.Fatalf("profile %d nested update = %T", profile, updates.Updates[0])
|
||
}
|
||
accepted, ok := encUpdate.Chat.(*tg.EncryptedChat)
|
||
if !ok {
|
||
t.Fatalf("profile %d chat = %T", profile, encUpdate.Chat)
|
||
}
|
||
if new(big.Int).SetBytes(accepted.GAOrB).Cmp(new(big.Int).SetBytes(gb)) != 0 {
|
||
t.Fatalf("profile %d changed g_b", profile)
|
||
}
|
||
if accepted.KeyFingerprint != fingerprint {
|
||
t.Fatalf("profile %d fingerprint = %d, want %d", profile, accepted.KeyFingerprint, fingerprint)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestEncryptedChatRPCHappyPath(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
ga := dhParam(0x55)
|
||
gb := dhParam(0x66)
|
||
|
||
// --- requestEncryption(发起方) ---
|
||
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: 777,
|
||
GA: ga,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("requestEncryption: %v", err)
|
||
}
|
||
waiting, ok := res.(*tg.EncryptedChatWaiting)
|
||
if !ok {
|
||
t.Fatalf("request response = %T, want *tg.EncryptedChatWaiting", res)
|
||
}
|
||
if waiting.ID != 777 {
|
||
t.Fatalf("waiting id = %d, want request random_id 777", waiting.ID)
|
||
}
|
||
// 推送给接受方的 encryptedChatRequested(携 g_a)。
|
||
recs := f.sessions.records()
|
||
if len(recs) != 1 || recs[0].userID != f.participant.ID {
|
||
t.Fatalf("request push = %+v, want single push to participant %d", recs, f.participant.ID)
|
||
}
|
||
requested, ok := encChatPayload(t, recs[0]).(*tg.EncryptedChatRequested)
|
||
if !ok {
|
||
t.Fatalf("participant payload = %T, want EncryptedChatRequested", encChatPayload(t, recs[0]))
|
||
}
|
||
if requested.ID != 777 {
|
||
t.Fatalf("participant requested id = %d, want request random_id 777", requested.ID)
|
||
}
|
||
if string(requested.GA) != string(ga) {
|
||
t.Fatal("requested g_a not relayed verbatim")
|
||
}
|
||
|
||
chat, found, _ := f.store.GetSecretChat(f.ctx, waiting.ID)
|
||
if !found {
|
||
t.Fatal("chat not persisted")
|
||
}
|
||
|
||
// --- acceptEncryption(接受方) ---
|
||
f.sessions.reset()
|
||
const fp = int64(0x0123456789abcdef)
|
||
accRes, err := f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
|
||
Peer: tg.InputEncryptedChat{ChatID: chat.ID, AccessHash: chat.ParticipantAccessHash},
|
||
GB: gb,
|
||
KeyFingerprint: fp,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("acceptEncryption: %v", err)
|
||
}
|
||
// 接受方同步响应:encryptedChat,GAOrB = g_a。
|
||
partView, ok := accRes.(*tg.EncryptedChat)
|
||
if !ok {
|
||
t.Fatalf("accept response = %T, want *tg.EncryptedChat", accRes)
|
||
}
|
||
if string(partView.GAOrB) != string(ga) {
|
||
t.Fatal("participant view GAOrB must be g_a")
|
||
}
|
||
if partView.KeyFingerprint != fp {
|
||
t.Fatalf("key fingerprint = %x, want %x", partView.KeyFingerprint, fp)
|
||
}
|
||
// 定向推送给发起设备 encryptedChat,并让 participant 其它设备收敛为 discarded。
|
||
recs = f.sessions.records()
|
||
if len(recs) != 2 {
|
||
t.Fatalf("accept pushes = %+v, want admin accepted + participant loser discarded", recs)
|
||
}
|
||
var adminRec, loserRec *phonePushRecord
|
||
for i := range recs {
|
||
switch recs[i].userID {
|
||
case f.admin.ID:
|
||
adminRec = &recs[i]
|
||
case f.participant.ID:
|
||
loserRec = &recs[i]
|
||
}
|
||
}
|
||
if adminRec == nil || adminRec.rawAuthKeyID != encAdminAuthKey {
|
||
t.Fatalf("admin accept push = %+v, want target auth key %x", adminRec, encAdminAuthKey)
|
||
}
|
||
if loserRec == nil || loserRec.rawAuthKeyID != encPartAuthKey {
|
||
t.Fatalf("loser discard push = %+v, want exclusion auth key %x", loserRec, encPartAuthKey)
|
||
}
|
||
adminView, ok := encChatPayload(t, *adminRec).(*tg.EncryptedChat)
|
||
if !ok {
|
||
t.Fatalf("admin payload = %T, want EncryptedChat", encChatPayload(t, *adminRec))
|
||
}
|
||
if string(adminView.GAOrB) != string(gb) {
|
||
t.Fatal("admin view GAOrB must be g_b")
|
||
}
|
||
if adminView.KeyFingerprint != fp {
|
||
t.Fatal("admin view key fingerprint not relayed byte-for-byte")
|
||
}
|
||
if discarded, ok := encChatPayload(t, *loserRec).(*tg.EncryptedChatDiscarded); !ok || !discarded.HistoryDeleted {
|
||
t.Fatalf("loser payload = %+v, want history-deleting EncryptedChatDiscarded", encChatPayload(t, *loserRec))
|
||
}
|
||
|
||
// --- discardEncryption(发起方) ---
|
||
f.sessions.reset()
|
||
okRes, err := f.router.onMessagesDiscardEncryption(f.adminCtx(), &tg.MessagesDiscardEncryptionRequest{
|
||
ChatID: chat.ID,
|
||
DeleteHistory: true,
|
||
})
|
||
if err != nil || !okRes {
|
||
t.Fatalf("discardEncryption: ok=%v err=%v", okRes, err)
|
||
}
|
||
recs = f.sessions.records()
|
||
if len(recs) != 1 || recs[0].userID != f.participant.ID {
|
||
t.Fatalf("discard push = %+v, want single push to participant", recs)
|
||
}
|
||
discarded, ok := encChatPayload(t, recs[0]).(*tg.EncryptedChatDiscarded)
|
||
if !ok || !discarded.HistoryDeleted {
|
||
t.Fatalf("discard payload = %+v, want EncryptedChatDiscarded{HistoryDeleted:true}", encChatPayload(t, recs[0]))
|
||
}
|
||
}
|
||
|
||
// TestDiscardSecretChatsForAuthKeyOnLogout 回归 P1:设备登出/授权撤销销毁其 perm auth_key 后,
|
||
// 必须级联 discard 该设备绑定的活跃密聊并向对端推送 encryptedChatDiscarded。修复前 onAuthLogOut
|
||
// 不处理密聊,对端继续往死 auth_key 投递成静默死链(消息 acked=f / qts 永久积压)。
|
||
func TestDiscardSecretChatsForAuthKeyOnLogout(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
ga := dhParam(0x55)
|
||
gb := dhParam(0x66)
|
||
|
||
// 建链到 normal:admin 发起 + participant 接受。
|
||
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: 11,
|
||
GA: ga,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
chatID := res.(*tg.EncryptedChatWaiting).ID
|
||
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
|
||
if _, err := f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
|
||
Peer: tg.InputEncryptedChat{ChatID: chat.ID, AccessHash: chat.ParticipantAccessHash},
|
||
GB: gb,
|
||
KeyFingerprint: 0x1234,
|
||
}); err != nil {
|
||
t.Fatalf("accept: %v", err)
|
||
}
|
||
if chat, _, _ := f.store.GetSecretChat(f.ctx, chatID); chat.State != domain.SecretChatStateNormal {
|
||
t.Fatalf("pre-logout state=%s, want normal", chat.State)
|
||
}
|
||
|
||
// 模拟 participant 设备登出:级联 discard 其 perm auth_key 绑定的密聊。
|
||
f.sessions.reset()
|
||
bobAuthKey := businessAuthKeyInt64(encPartAuthKey)
|
||
f.router.discardSecretChatsForAuthKey(f.ctx, bobAuthKey, f.participant.ID)
|
||
|
||
// [1] 密聊已迁移到 discarded(不再是 normal 死链)。
|
||
if chat, _, _ := f.store.GetSecretChat(f.ctx, chatID); chat.State != domain.SecretChatStateDiscarded {
|
||
t.Fatalf("post-logout state=%s, want discarded", chat.State)
|
||
}
|
||
// [2] 对端 admin 收到单条 encryptedChatDiscarded 在线推送。
|
||
recs := f.sessions.records()
|
||
if len(recs) != 1 || recs[0].userID != f.admin.ID {
|
||
t.Fatalf("discard push = %+v, want single push to admin %d", recs, f.admin.ID)
|
||
}
|
||
if _, ok := encChatPayload(t, recs[0]).(*tg.EncryptedChatDiscarded); !ok {
|
||
t.Fatalf("peer payload = %T, want EncryptedChatDiscarded", encChatPayload(t, recs[0]))
|
||
}
|
||
// [3] durable 离线补偿事件写给对端设备(getDifference 兜底)。
|
||
if events, err := f.queue.ListUndeliveredStateEvents(f.ctx, f.admin.ID, businessAuthKeyInt64(encAdminAuthKey), 10); err != nil || len(events) == 0 {
|
||
t.Fatalf("durable discard event for admin = %d (err=%v), want >=1", len(events), err)
|
||
}
|
||
|
||
// [4] 幂等:再次对同 auth_key 登出不再 discard、不再推送(已是终态)。
|
||
f.sessions.reset()
|
||
f.router.discardSecretChatsForAuthKey(f.ctx, bobAuthKey, f.participant.ID)
|
||
if recs := f.sessions.records(); len(recs) != 0 {
|
||
t.Fatalf("idempotent re-logout pushed %d updates, want 0", len(recs))
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionSelf(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
_, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.admin.ID, AccessHash: f.admin.AccessHash},
|
||
RandomID: 1,
|
||
GA: dhParam(0x55),
|
||
})
|
||
assertPhoneRPCErr(t, err, "USER_ID_INVALID")
|
||
}
|
||
|
||
func TestRequestEncryptionRandomIDContractRPC(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
request := func(randomID int) (tg.EncryptedChatClass, error) {
|
||
return f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: randomID,
|
||
GA: dhParam(0x55),
|
||
})
|
||
}
|
||
|
||
negative, err := request(-808)
|
||
if err != nil {
|
||
t.Fatalf("negative random_id: %v", err)
|
||
}
|
||
if got := negative.(*tg.EncryptedChatWaiting).ID; got != -808 {
|
||
t.Fatalf("negative waiting id = %d, want -808", got)
|
||
}
|
||
negativeChat, found, err := f.store.GetSecretChat(f.ctx, -808)
|
||
if err != nil || !found {
|
||
t.Fatalf("stored negative chat: found=%v err=%v", found, err)
|
||
}
|
||
accepted, err := f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
|
||
Peer: tg.InputEncryptedChat{ChatID: -808, AccessHash: negativeChat.ParticipantAccessHash},
|
||
GB: dhParam(0x66),
|
||
KeyFingerprint: 0x1234,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("accept negative chat id: %v", err)
|
||
}
|
||
if got := accepted.(*tg.EncryptedChat).ID; got != -808 {
|
||
t.Fatalf("accepted id = %d, want -808", got)
|
||
}
|
||
|
||
if _, err := request(0); err == nil {
|
||
t.Fatal("zero random_id succeeded, want RANDOM_ID_DUPLICATE")
|
||
} else {
|
||
assertPhoneRPCErr(t, err, "RANDOM_ID_DUPLICATE")
|
||
}
|
||
|
||
// 同一全局 chat_id 改变握手意图不能被幂等吞掉。
|
||
changed := &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: -808,
|
||
GA: dhParam(0x66),
|
||
}
|
||
if _, err := f.router.onMessagesRequestEncryption(f.adminCtx(), changed); err == nil {
|
||
t.Fatal("changed intent succeeded, want RANDOM_ID_DUPLICATE")
|
||
} else {
|
||
assertPhoneRPCErr(t, err, "RANDOM_ID_DUPLICATE")
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionWrongAccessHashRPC(t *testing.T) {
|
||
f := newEncryptedFixture(t)
|
||
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
|
||
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
|
||
RandomID: 9,
|
||
GA: dhParam(0x55),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
chatID := res.(*tg.EncryptedChatWaiting).ID
|
||
_, err = f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
|
||
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: 999999},
|
||
GB: dhParam(0x66),
|
||
KeyFingerprint: 1,
|
||
})
|
||
assertPhoneRPCErr(t, err, "CHAT_ID_INVALID")
|
||
}
|