Files
olons_server/internal/httpapi/uid.go
T

143 lines
2.8 KiB
Go

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
}