package httpapi import ( "encoding/base64" "encoding/binary" "encoding/json" "fmt" "strconv" "strings" "sync" "golang.org/x/crypto/xtea" ) type userID int64 const defaultUIDKey = "la6YsO+bNX/+XIkOqc5Svw==" var ( uidCodecMu sync.RWMutex uidCipher *xtea.Cipher ) func init() { if err := SetUIDKey(defaultUIDKey); err != nil { panic(err) } } func SetUIDKey(encoded string) error { key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded)) if err != nil { return fmt.Errorf("invalid uid key: %w", err) } cipher, err := xtea.NewCipher(key) if err != nil { return fmt.Errorf("invalid uid key: %w", err) } uidCodecMu.Lock() uidCipher = cipher uidCodecMu.Unlock() return nil } func (id *userID) UnmarshalJSON(raw []byte) error { value, err := parseUserIDJSON(raw) if err != nil { return err } *id = userID(value) return nil } type optionalUserID struct { value int64 set bool } func (id *optionalUserID) UnmarshalJSON(raw []byte) error { if strings.TrimSpace(string(raw)) == "null" { return nil } value, err := parseUserIDJSON(raw) if err != nil { return err } id.set = true id.value = value return nil } func (id optionalUserID) ptr() *int64 { if !id.set { return nil } return &id.value } func parseUserIDJSON(raw []byte) (int64, error) { var text string if err := json.Unmarshal(raw, &text); err == nil { return parseUserID(text) } return 0, fmt.Errorf("user id must be a string") } func parseUserID(value string) (int64, error) { value = strings.TrimSpace(value) if value == "" { return 0, fmt.Errorf("empty user id") } if strings.HasPrefix(value, "usr") { decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(strings.TrimPrefix(value, "usr")) if err != nil || len(decoded) < 8 { return 0, fmt.Errorf("invalid user id") } return decryptUID(decoded[:8]), nil } return strconv.ParseInt(value, 10, 64) } func formatUserID(id int64) string { if id == 0 { return "" } encoded := encryptUID(id) return "usr" + base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(encoded) } func parseUserIDList(values []userID) []int64 { items := make([]int64, 0, len(values)) seen := make(map[int64]bool, len(values)) for _, value := range values { id := int64(value) if id == 0 || seen[id] { continue } seen[id] = true items = append(items, id) } return items } func decryptUID(encoded []byte) int64 { dst := make([]byte, 8) uidCodecMu.RLock() cipher := uidCipher uidCodecMu.RUnlock() cipher.Decrypt(dst, encoded[:8]) return int64(binary.LittleEndian.Uint64(dst)) } func encryptUID(id int64) []byte { src := make([]byte, 8) dst := make([]byte, 8) binary.LittleEndian.PutUint64(src, uint64(id)) uidCodecMu.RLock() cipher := uidCipher uidCodecMu.RUnlock() cipher.Encrypt(dst, src) return dst }