mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
feat(idcodec): add hashid obfuscation derived from jwt secret
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>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user