owpengram-server/internal/domain/auth_delivery_report.go
2026-07-24 11:56:59 +08:00

114 lines
3.2 KiB
Go

package domain
import (
"crypto/sha256"
"encoding/json"
"errors"
"time"
"unicode/utf8"
)
const (
MaxAuthDeliveryMNCBytes = 8
MaxAuthDeliveryClientTypeBytes = 32
MaxAuthDeliveryIDBytes = 128
MaxAuthDeliveryReportsPerHour = 10
MaxAuthDeliveryReportsPerPhoneDay = 20
)
var (
ErrAuthDeliveryReportInvalid = errors.New("auth delivery report invalid")
ErrAuthDeliveryRateLimited = errors.New("auth delivery report rate limited")
)
// AuthDeliveryReport is operational delivery telemetry, not an abuse report.
// It deliberately stores only hashes of the phone and phone_code_hash and
// never stores the authentication code.
type AuthDeliveryReport struct {
ID int64
AuthKeyID [8]byte
SessionID int64
ClientType string
PhoneHash [sha256.Size]byte
CodeHash [sha256.Size]byte
IssuedUserID int64
DeliveryID string
Channel AuthCodeDeliveryKind
MNC string
Fingerprint [sha256.Size]byte
CreatedAt time.Time
}
type AuthMissingCodeReportRequest struct {
AuthKeyID [8]byte
SessionID int64
ClientType string
Phone string
PhoneCodeHash string
MNC string
CreatedAt time.Time
}
func NewAuthDeliveryReport(authKeyID [8]byte, sessionID int64, clientType, phone, phoneCodeHash string, issuedUserID int64, deliveryID string, channel AuthCodeDeliveryKind, mnc string, createdAt time.Time) (AuthDeliveryReport, error) {
report := AuthDeliveryReport{
AuthKeyID: authKeyID, SessionID: sessionID, ClientType: clientType,
PhoneHash: sha256.Sum256([]byte(phone)), CodeHash: sha256.Sum256([]byte(phoneCodeHash)),
IssuedUserID: issuedUserID, DeliveryID: deliveryID,
Channel: channel, MNC: mnc, CreatedAt: createdAt,
}
raw, err := json.Marshal(struct {
Version int
AuthKeyID [8]byte
SessionID int64
PhoneHash [sha256.Size]byte
CodeHash [sha256.Size]byte
DeliveryID string
Channel AuthCodeDeliveryKind
MNC string
}{
Version: 1, AuthKeyID: authKeyID, SessionID: sessionID,
PhoneHash: report.PhoneHash, CodeHash: report.CodeHash,
DeliveryID: deliveryID, Channel: channel, MNC: mnc,
})
if err != nil {
return AuthDeliveryReport{}, ErrAuthDeliveryReportInvalid
}
report.Fingerprint = sha256.Sum256(raw)
if err := report.Validate(); err != nil {
return AuthDeliveryReport{}, err
}
return report, nil
}
func (r AuthDeliveryReport) Validate() error {
if r.ID < 0 || r.AuthKeyID == ([8]byte{}) || r.SessionID == 0 ||
r.PhoneHash == ([sha256.Size]byte{}) || r.CodeHash == ([sha256.Size]byte{}) ||
r.Fingerprint == ([sha256.Size]byte{}) || r.IssuedUserID < 0 ||
len(r.ClientType) > MaxAuthDeliveryClientTypeBytes || !utf8.ValidString(r.ClientType) ||
len(r.DeliveryID) > MaxAuthDeliveryIDBytes || !utf8.ValidString(r.DeliveryID) ||
!validAuthDeliveryChannel(r.Channel) || !validMNC(r.MNC) || r.CreatedAt.IsZero() {
return ErrAuthDeliveryReportInvalid
}
return nil
}
func validAuthDeliveryChannel(channel AuthCodeDeliveryKind) bool {
switch channel {
case AuthCodeDeliveryPhone, AuthCodeDeliverySMS:
return true
default:
return false
}
}
func validMNC(mnc string) bool {
if len(mnc) > MaxAuthDeliveryMNCBytes {
return false
}
for _, r := range mnc {
if r < '0' || r > '9' {
return false
}
}
return true
}