From 798ea70b4eca238b71d3c417618e461bb3bb8234 Mon Sep 17 00:00:00 2001 From: Astra Date: Sun, 13 Sep 2026 22:07:54 +0100 Subject: [PATCH] rpc: return PASSWORD_MISSING for channel transfer without 2FA messages.editChatCreator unconditionally returned PASSWORD_HASH_INVALID for an account with no cloud password at all. Real Telegram Desktop's transfer- ownership flow only recognizes the distinct PASSWORD_MISSING error to show its "enable 2FA first" box; anything else falls through into the real password-entry flow, which then has nothing to check against and crashes. Co-Authored-By: Claude Sonnet 5 --- internal/domain/account.go | 1 + internal/rpc/channels_leave_rpc_test.go | 57 +++++++++++++++++++++++++ internal/rpc/channels_legacy_chat.go | 17 ++++++-- internal/rpc/errors.go | 3 ++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/internal/domain/account.go b/internal/domain/account.go index 77369673..7ca459e2 100644 --- a/internal/domain/account.go +++ b/internal/domain/account.go @@ -7,6 +7,7 @@ import ( var ( ErrPasswordHashInvalid = errors.New("password hash invalid") + ErrPasswordMissing = errors.New("password missing") ErrSRPIDInvalid = errors.New("srp id invalid") ErrSRPPasswordChanged = errors.New("srp password changed") ErrNewSettingsInvalid = errors.New("new password settings invalid") diff --git a/internal/rpc/channels_leave_rpc_test.go b/internal/rpc/channels_leave_rpc_test.go index 22c09360..a2f3acee 100644 --- a/internal/rpc/channels_leave_rpc_test.go +++ b/internal/rpc/channels_leave_rpc_test.go @@ -9,6 +9,7 @@ import ( "github.com/iamxvbaba/td/tgerr" "go.uber.org/zap/zaptest" + appaccount "telesrv/internal/app/account" appchannels "telesrv/internal/app/channels" appusers "telesrv/internal/app/users" "telesrv/internal/domain" @@ -26,6 +27,10 @@ func (acceptPasswordAccountService) CheckPassword(_ context.Context, _ int64, ch return nil } +func (acceptPasswordAccountService) GetPassword(_ context.Context, _ int64) (domain.PasswordSettings, error) { + return domain.PasswordSettings{HasPassword: true}, nil +} + func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() @@ -242,6 +247,58 @@ func TestMessagesEditChatCreatorTransfersWithoutChannelPts(t *testing.T) { } } +// TestMessagesEditChatCreatorRequiresPasswordSetup covers an owner who has never +// enabled two-step verification: the client's transfer-ownership probe +// (inputUserEmpty + inputCheckPasswordEmpty) must get PASSWORD_MISSING, not +// PASSWORD_HASH_INVALID -- the desktop client only recognizes PASSWORD_MISSING +// to show its "enable 2FA first" box, and otherwise falls through into a real +// password-entry flow it can't satisfy. +func TestMessagesEditChatCreatorRequiresPasswordSetup(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, err := userStore.Create(ctx, domain.User{AccessHash: 9221, Phone: "15550009221", FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + member, err := userStore.Create(ctx, domain.User{AccessHash: 9222, Phone: "15550009222", FirstName: "Member"}) + if err != nil { + t.Fatalf("create member: %v", err) + } + channelStore := memory.NewChannelStore() + channelService := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Account: appaccount.NewService(memory.NewPasswordStore()), + Users: appusers.NewService(userStore), + Channels: channelService, + }, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700009130, 0)}) + created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{ + CreatorUserID: owner.ID, + Title: "no password owner", + Megagroup: true, + MemberUserIDs: []int64{member.ID}, + Date: 1700009130, + }) + if err != nil { + t.Fatalf("create channel: %v", err) + } + ownerCtx := WithUserID(ctx, owner.ID) + peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + if _, err := r.onMessagesEditChatCreator(ownerCtx, &tg.MessagesEditChatCreatorRequest{ + Peer: peer, + UserID: &tg.InputUserEmpty{}, + Password: &tg.InputCheckPasswordEmpty{}, + }); err == nil || !tgerr.Is(err, "PASSWORD_MISSING") { + t.Fatalf("editChatCreator probe err = %v, want PASSWORD_MISSING", err) + } + if _, err := r.onMessagesEditChatCreator(ownerCtx, &tg.MessagesEditChatCreatorRequest{ + Peer: peer, + UserID: &tg.InputUser{UserID: member.ID, AccessHash: member.AccessHash}, + Password: &tg.InputCheckPasswordSRP{SRPID: 1, A: []byte{1}, M1: []byte{2}}, + }); err == nil || !tgerr.Is(err, "PASSWORD_MISSING") { + t.Fatalf("editChatCreator transfer err = %v, want PASSWORD_MISSING", err) + } +} + func TestMessagesGetFutureChatCreatorAfterLeaveNoCandidate(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() diff --git a/internal/rpc/channels_legacy_chat.go b/internal/rpc/channels_legacy_chat.go index b3d653ff..999a3dbc 100644 --- a/internal/rpc/channels_legacy_chat.go +++ b/internal/rpc/channels_legacy_chat.go @@ -339,6 +339,20 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages if req.Password == nil { return nil, passwordHashInvalidErr() } + if r.deps.Account == nil { + return nil, passwordHashInvalidErr() + } + // 转让所有权无条件要求已开启两步验证:先探测账号是否设有密码, + // 让 messages.editChatCreator 的探测请求(inputUserEmpty + inputCheckPasswordEmpty) + // 拿到 PASSWORD_MISSING 而非 PASSWORD_HASH_INVALID —— 客户端只识别前者来展示 + // “请先开启两步验证”提示,否则会误入真实密码校验流程并在没有密码可核对时崩溃。 + passwordSettings, err := r.deps.Account.GetPassword(ctx, userID) + if err != nil { + return nil, internalErr() + } + if !passwordSettings.HasPassword { + return nil, passwordMissingErr() + } if _, ok := req.UserID.(*tg.InputUserEmpty); ok { return nil, passwordHashInvalidErr() } @@ -355,9 +369,6 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages if target.Bot { return nil, userIDInvalidErr() } - if r.deps.Account == nil { - return nil, passwordHashInvalidErr() - } if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil { return nil, passwordErr(err) } diff --git a/internal/rpc/errors.go b/internal/rpc/errors.go index 564bb558..197ca0d0 100644 --- a/internal/rpc/errors.go +++ b/internal/rpc/errors.go @@ -291,6 +291,7 @@ func scoreInvalidErr() error { return tgerr.New(400, "SCORE_INVALID") } func sessionPasswordNeededErr() error { return tgerr.New(401, "SESSION_PASSWORD_NEEDED") } func passwordHashInvalidErr() error { return tgerr.New(400, "PASSWORD_HASH_INVALID") } +func passwordMissingErr() error { return tgerr.New(400, "PASSWORD_MISSING") } func srpIDInvalidErr() error { return tgerr.New(400, "SRP_ID_INVALID") } func srpPasswordChangedErr() error { return tgerr.New(400, "SRP_PASSWORD_CHANGED") } func newSettingsInvalidErr() error { return tgerr.New(400, "NEW_SETTINGS_INVALID") } @@ -468,6 +469,8 @@ func passwordErr(err error) error { switch { case errors.Is(err, domain.ErrPasswordHashInvalid): return passwordHashInvalidErr() + case errors.Is(err, domain.ErrPasswordMissing): + return passwordMissingErr() case errors.Is(err, domain.ErrSRPIDInvalid): return srpIDInvalidErr() case errors.Is(err, domain.ErrSRPPasswordChanged):