fix for phone generation on email signup

This commit is contained in:
onysd 2026-07-14 00:30:52 +03:00
parent cee960fea0
commit 3409f190b9
20 changed files with 457 additions and 54 deletions

View file

@ -1,6 +1,10 @@
package domain
import "strings"
import (
"crypto/rand"
"fmt"
"strings"
)
// EmailPhonePrefix marks a "phone number" as a synthetic identity encoding an
// email address, not a real phone. It reuses Telegram's own +888 "Anonymous
@ -24,11 +28,11 @@ const MaxEmailSignupPhoneLen = 200
const emailPhoneEscape = 'q'
var emailPhoneEscapeEncode = map[rune]byte{
'@': '0',
'.': '1',
'-': '2',
'_': '3',
'+': '4',
'@': '0',
'.': '1',
'-': '2',
'_': '3',
'+': '4',
emailPhoneEscape: '5',
}
@ -135,6 +139,33 @@ func NormalizeEmailForPhone(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
// emailSignupDisplayPhoneDigits is how many random digits follow the "888"
// prefix in a NewEmailSignupDisplayPhone result, e.g. "888" + 8 digits =
// "88812345678" (formats on-screen as something like "+888 1234 5678").
const emailSignupDisplayPhoneDigits = 8
// NewEmailSignupDisplayPhone generates a short, all-digit "888" phone number
// for an email-signup account's users.phone column. Unlike EncodeEmailPhone
// this carries no information about the email — it is purely a
// normal-looking display/identity number — so the caller must separately
// persist the email->user association (see User.SignupEmail) for returning
// logins to be found. Because the result is all digits, IsEmailSignupPhone
// on it is always false: once assigned, it behaves exactly like a real phone
// number everywhere else in the system (contacts, search, ByPhone lookups).
func NewEmailSignupDisplayPhone() (string, error) {
b := make([]byte, emailSignupDisplayPhoneDigits)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate email signup display phone: %w", err)
}
var out strings.Builder
out.Grow(len(EmailPhonePrefix) + emailSignupDisplayPhoneDigits)
out.WriteString(EmailPhonePrefix)
for _, v := range b {
out.WriteByte('0' + v%10)
}
return out.String(), nil
}
// IsEmailSignupPhone reports whether phone was produced by EncodeEmailPhone.
// Every encoded value contains at least one letter (the mandatory '@'
// escape's 'q' marker byte), which real, all-digit phone numbers — even

View file

@ -1,6 +1,9 @@
package domain
import "testing"
import (
"strings"
"testing"
)
func TestEncodeDecodeEmailPhoneRoundTrip(t *testing.T) {
for _, email := range []string{
@ -62,3 +65,32 @@ func TestDecodeEmailPhoneRejectsNonEmailNumbers(t *testing.T) {
}
}
}
func TestNewEmailSignupDisplayPhoneLooksLikeARealPhoneNumber(t *testing.T) {
seen := make(map[string]struct{})
for i := 0; i < 200; i++ {
phone, err := NewEmailSignupDisplayPhone()
if err != nil {
t.Fatalf("NewEmailSignupDisplayPhone: %v", err)
}
if !strings.HasPrefix(phone, EmailPhonePrefix) {
t.Fatalf("phone %q missing %q prefix", phone, EmailPhonePrefix)
}
if !ValidPhone(phone) {
t.Fatalf("phone %q fails ValidPhone", phone)
}
// Must be indistinguishable from a real phone number: no letters, so
// IsEmailSignupPhone/DecodeEmailPhone never mistake it for a wire
// email-signup value once it's assigned as an account's real phone.
if IsEmailSignupPhone(phone) {
t.Fatalf("IsEmailSignupPhone(%q) = true, want false (must look like a real number)", phone)
}
if _, ok := DecodeEmailPhone(phone); ok {
t.Fatalf("DecodeEmailPhone(%q) unexpectedly succeeded", phone)
}
seen[phone] = struct{}{}
}
if len(seen) < 190 {
t.Fatalf("only %d distinct values out of 200 draws, generator looks non-random", len(seen))
}
}

View file

@ -9,6 +9,11 @@ type PhoneChangeRequest struct {
Date int
ExcludeAuthKeyID [8]byte
ExcludeSessionID int64
// SignupEmail, when non-empty, is written to users.signup_email in the
// same transaction as Phone. Only email-signup phone changes set this
// (see account.Service.ChangePhone); ordinary phone-number changes leave
// it as the zero value and the column untouched.
SignupEmail string
}
type PhoneChangeResult struct {

View file

@ -22,9 +22,14 @@ func (c PeerColor) Empty() bool {
// User 是一个账号。第一阶段仅保留登录链路必须字段;
// access_hash 为任何 InputUser 校验所必须,不可省。
type User struct {
ID int64
AccessHash int64
Phone string
ID int64
AccessHash int64
Phone string
// SignupEmail is set only for email-signup accounts (see
// domain.NewEmailSignupDisplayPhone): it is the durable email->user
// reverse lookup key, since Phone itself no longer encodes the email.
// Empty for every ordinary phone-number account.
SignupEmail string
FirstName string
LastName string
About string

View file

@ -30,10 +30,12 @@ func OfficialWelcomeMessage(userID int64, method string, date int) (Message, err
}
// SignInMethodLabel returns the human-readable method name embedded in
// OfficialWelcomeMessage, derived from whether phone is an email-signup
// synthetic number (see EncodeEmailPhone) or a real phone number.
func SignInMethodLabel(phone string) string {
if IsEmailSignupPhone(phone) {
// OfficialWelcomeMessage. Email-signup accounts are identified by
// SignupEmail (see NewEmailSignupDisplayPhone) rather than by their stored
// Phone, which — once assigned — is an ordinary-looking short number that
// carries no information about the signup method.
func SignInMethodLabel(u User) string {
if u.SignupEmail != "" {
return "email"
}
return "phone number"