better fix

This commit is contained in:
onysd 2026-07-31 16:57:57 +03:00
parent 87a2a2b0e2
commit 84063d75fc
7 changed files with 128 additions and 13 deletions

View file

@ -863,17 +863,34 @@ func (s *Service) CancelCodeForAuthKey(ctx context.Context, authKeyID [8]byte, p
return s.cancelCode(ctx, authKeyID, phone, phoneCodeHash)
}
// LoginEmailResetAvailable reports whether auth.resetLoginEmail's SMS
// fallback could actually succeed on this deployment -- the exact same
// condition ConsumeLoginEmailReset enforces. The RPC layer uses this to
// decide whether to advertise reset_available_period at all, so the client
// never offers a "Can't access this email?" escape hatch that can only ever
// fail (or, before this was locked down, silently succeed with the
// well-known fixed dev code).
func (s *Service) LoginEmailResetAvailable() bool {
return s.phoneCodeSender != nil && !s.emailSignupEnabled
}
// ConsumeLoginEmailReset authorizes auth.resetLoginEmail with the exact
// email-login hash previously issued for this phone owner. Possession of only
// a phone number is never sufficient to remove an authentication factor.
func (s *Service) ConsumeLoginEmailReset(ctx context.Context, phone, phoneCodeHash string) (int64, error) {
// This flow exists to fall back to an SMS code when the login email is
// unreachable. Without a real phoneCodeSender configured, that "SMS code"
// is always the well-known TELESRV_DEV_AUTH_CODE (see createPhoneCode),
// so anyone who can call sendCode for a phone (no email access required)
// could strip the login-email requirement with a publicly known code.
// Refuse up front, before ClearLoginEmail runs, so nothing is mutated.
if s.phoneCodeSender == nil {
// unreachable. Two independent reasons it must refuse outright, before
// ClearLoginEmail runs so nothing is ever mutated on a doomed request:
// - no real phoneCodeSender: the "SMS code" is always the well-known
// TELESRV_DEV_AUTH_CODE (see createPhoneCode), so anyone who can call
// sendCode for a phone (no email access required) could strip the
// login-email requirement with a publicly known code.
// - emailSignupEnabled: this account's "phone" is a synthetic 888-
// prefixed display number (domain.NewEmailSignupDisplayPhone), never
// a real number anyone can receive SMS on. Email is the actual
// identity here regardless of whether a real SMS sender happens to
// be configured for other (real-phone) accounts on this server.
if s.phoneCodeSender == nil || s.emailSignupEnabled {
return 0, ErrCodeInvalid
}
phone = normalizePhone(phone)

View file

@ -391,6 +391,44 @@ func TestEmailSetupVerificationAuthorizesSignUpWithWelcomeMessageOnlyNoCodeEcho(
}
}
// TestLoginEmailResetUnavailableForEmailSignupAccounts locks down that the
// SMS-fallback reset must stay refused for email-signup accounts even with a
// real phoneCodeSender configured: their "phone" is a synthetic display
// number nobody can receive SMS on, so email is the only real identity
// factor and must never be strippable via this escape hatch.
func TestLoginEmailResetUnavailableForEmailSignupAccounts(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
owner, err := users.Create(ctx, domain.User{Phone: "88800009999", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
codes := memory.NewCodeStore()
hash := "email-signup-reset"
if err := codes.Set(ctx, hash, store.PhoneCode{
Version: store.PhoneCodeVersionCurrent,
IssuedUserID: owner.ID,
Phone: owner.Phone,
Code: "654321",
Channel: codeChannelEmailLogin,
MaxAttempts: 5,
}, time.Minute); err != nil {
t.Fatalf("seed code: %v", err)
}
svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345",
WithPhoneCodeDelivery(&captureOTPSender{}, 5), WithEmailSignup(true))
if svc.LoginEmailResetAvailable() {
t.Fatalf("LoginEmailResetAvailable = true, want false for an email-signup deployment")
}
if _, err := svc.ConsumeLoginEmailReset(ctx, owner.Phone, hash); !errors.Is(err, ErrCodeInvalid) {
t.Fatalf("ConsumeLoginEmailReset err=%v, want ErrCodeInvalid", err)
}
if _, found, err := codes.Get(ctx, hash); err != nil || !found {
t.Fatalf("reset probe destroyed the seeded code found=%v err=%v", found, err)
}
}
func TestConsumeLoginEmailResetRequiresExactIssuedHash(t *testing.T) {
ctx := context.Background()
baseUsers := memory.NewUserStore()