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:
parent
7f0921c82c
commit
798ea70b4e
4 changed files with 75 additions and 3 deletions
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrPasswordHashInvalid = errors.New("password hash invalid")
|
ErrPasswordHashInvalid = errors.New("password hash invalid")
|
||||||
|
ErrPasswordMissing = errors.New("password missing")
|
||||||
ErrSRPIDInvalid = errors.New("srp id invalid")
|
ErrSRPIDInvalid = errors.New("srp id invalid")
|
||||||
ErrSRPPasswordChanged = errors.New("srp password changed")
|
ErrSRPPasswordChanged = errors.New("srp password changed")
|
||||||
ErrNewSettingsInvalid = errors.New("new password settings invalid")
|
ErrNewSettingsInvalid = errors.New("new password settings invalid")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/tgerr"
|
"github.com/iamxvbaba/td/tgerr"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
appaccount "telesrv/internal/app/account"
|
||||||
appchannels "telesrv/internal/app/channels"
|
appchannels "telesrv/internal/app/channels"
|
||||||
appusers "telesrv/internal/app/users"
|
appusers "telesrv/internal/app/users"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
|
|
@ -26,6 +27,10 @@ func (acceptPasswordAccountService) CheckPassword(_ context.Context, _ int64, ch
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (acceptPasswordAccountService) GetPassword(_ context.Context, _ int64) (domain.PasswordSettings, error) {
|
||||||
|
return domain.PasswordSettings{HasPassword: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testing.T) {
|
func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userStore := memory.NewUserStore()
|
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) {
|
func TestMessagesGetFutureChatCreatorAfterLeaveNoCandidate(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userStore := memory.NewUserStore()
|
userStore := memory.NewUserStore()
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,20 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages
|
||||||
if req.Password == nil {
|
if req.Password == nil {
|
||||||
return nil, passwordHashInvalidErr()
|
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 {
|
if _, ok := req.UserID.(*tg.InputUserEmpty); ok {
|
||||||
return nil, passwordHashInvalidErr()
|
return nil, passwordHashInvalidErr()
|
||||||
}
|
}
|
||||||
|
|
@ -355,9 +369,6 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages
|
||||||
if target.Bot {
|
if target.Bot {
|
||||||
return nil, userIDInvalidErr()
|
return nil, userIDInvalidErr()
|
||||||
}
|
}
|
||||||
if r.deps.Account == nil {
|
|
||||||
return nil, passwordHashInvalidErr()
|
|
||||||
}
|
|
||||||
if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil {
|
if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil {
|
||||||
return nil, passwordErr(err)
|
return nil, passwordErr(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ func scoreInvalidErr() error { return tgerr.New(400, "SCORE_INVALID") }
|
||||||
|
|
||||||
func sessionPasswordNeededErr() error { return tgerr.New(401, "SESSION_PASSWORD_NEEDED") }
|
func sessionPasswordNeededErr() error { return tgerr.New(401, "SESSION_PASSWORD_NEEDED") }
|
||||||
func passwordHashInvalidErr() error { return tgerr.New(400, "PASSWORD_HASH_INVALID") }
|
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 srpIDInvalidErr() error { return tgerr.New(400, "SRP_ID_INVALID") }
|
||||||
func srpPasswordChangedErr() error { return tgerr.New(400, "SRP_PASSWORD_CHANGED") }
|
func srpPasswordChangedErr() error { return tgerr.New(400, "SRP_PASSWORD_CHANGED") }
|
||||||
func newSettingsInvalidErr() error { return tgerr.New(400, "NEW_SETTINGS_INVALID") }
|
func newSettingsInvalidErr() error { return tgerr.New(400, "NEW_SETTINGS_INVALID") }
|
||||||
|
|
@ -468,6 +469,8 @@ func passwordErr(err error) error {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, domain.ErrPasswordHashInvalid):
|
case errors.Is(err, domain.ErrPasswordHashInvalid):
|
||||||
return passwordHashInvalidErr()
|
return passwordHashInvalidErr()
|
||||||
|
case errors.Is(err, domain.ErrPasswordMissing):
|
||||||
|
return passwordMissingErr()
|
||||||
case errors.Is(err, domain.ErrSRPIDInvalid):
|
case errors.Is(err, domain.ErrSRPIDInvalid):
|
||||||
return srpIDInvalidErr()
|
return srpIDInvalidErr()
|
||||||
case errors.Is(err, domain.ErrSRPPasswordChanged):
|
case errors.Is(err, domain.ErrSRPPasswordChanged):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue