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 <noreply@anthropic.com>
This commit is contained in:
Astra 2026-09-13 22:07:54 +01:00
parent 7f0921c82c
commit 798ea70b4e
4 changed files with 75 additions and 3 deletions

View file

@ -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()