mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
New pkg/idcodec wraps sqids with an alphabet derived from the JWT secret via HKDF-SHA256 (info="nezha/idcodec/alphabet/v1"). Rotating NZ_JWTSECRETKEY automatically reshuffles the alphabet, which doubles as a kill switch for outstanding hashids without touching the encoder itself. The base alphabet drops visually-confusable characters (0/O/o/I/l/1) and MinLength=8 hides small integer ids. Decode round-trips through Encode to reject inputs that decode by accident under the same alphabet. Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
104 lines
2.0 KiB
Go
104 lines
2.0 KiB
Go
package idcodec
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/sqids/sqids-go"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
const (
|
|
baseAlphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
hkdfInfo = "nezha/idcodec/alphabet/v1"
|
|
minLength = 8
|
|
minMasterKey = 32
|
|
)
|
|
|
|
var (
|
|
ErrNotInitialized = errors.New("idcodec: not initialized")
|
|
ErrInvalidCode = errors.New("idcodec: invalid id code")
|
|
ErrMasterKeyShort = errors.New("idcodec: master key too short")
|
|
|
|
mu sync.RWMutex
|
|
encoder *sqids.Sqids
|
|
)
|
|
|
|
func Init(masterKey []byte) error {
|
|
if len(masterKey) < minMasterKey {
|
|
return ErrMasterKeyShort
|
|
}
|
|
alphaKey := make([]byte, 32)
|
|
if _, err := io.ReadFull(hkdf.New(sha256.New, masterKey, nil, []byte(hkdfInfo)), alphaKey); err != nil {
|
|
return err
|
|
}
|
|
enc, err := sqids.New(sqids.Options{
|
|
Alphabet: keyedShuffle(baseAlphabet, alphaKey),
|
|
MinLength: minLength,
|
|
Blocklist: []string{},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mu.Lock()
|
|
encoder = enc
|
|
mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func Encode(id uint64) (string, error) {
|
|
mu.RLock()
|
|
enc := encoder
|
|
mu.RUnlock()
|
|
if enc == nil {
|
|
return "", ErrNotInitialized
|
|
}
|
|
return enc.Encode([]uint64{id})
|
|
}
|
|
|
|
func Decode(code string) (uint64, error) {
|
|
mu.RLock()
|
|
enc := encoder
|
|
mu.RUnlock()
|
|
if enc == nil {
|
|
return 0, ErrNotInitialized
|
|
}
|
|
nums := enc.Decode(code)
|
|
if len(nums) != 1 {
|
|
return 0, ErrInvalidCode
|
|
}
|
|
if got, err := enc.Encode(nums); err != nil || got != code {
|
|
return 0, ErrInvalidCode
|
|
}
|
|
return nums[0], nil
|
|
}
|
|
|
|
func keyedShuffle(alphabet string, key []byte) string {
|
|
runes := []rune(alphabet)
|
|
mac := hmac.New(sha256.New, key)
|
|
var counter uint64
|
|
var pool []byte
|
|
next := func() byte {
|
|
if len(pool) == 0 {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, counter)
|
|
counter++
|
|
mac.Reset()
|
|
mac.Write(buf)
|
|
pool = mac.Sum(nil)
|
|
}
|
|
b := pool[0]
|
|
pool = pool[1:]
|
|
return b
|
|
}
|
|
for i := len(runes) - 1; i > 0; i-- {
|
|
j := int(next()) % (i + 1)
|
|
runes[i], runes[j] = runes[j], runes[i]
|
|
}
|
|
return string(runes)
|
|
}
|