mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/foundation/tokenhash"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type MasterService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewMasterService(db *gorm.DB) *MasterService {
|
|
return &MasterService{db: db}
|
|
}
|
|
|
|
func (s *MasterService) CreateMaster(name, group string, maxChildKeys, globalQPS int) (*model.Master, string, error) {
|
|
rawMasterKey, err := generateRandomKey(32)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to generate master key: %w", err)
|
|
}
|
|
|
|
hashedMasterKey, err := bcrypt.GenerateFromPassword([]byte(rawMasterKey), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to hash master key: %w", err)
|
|
}
|
|
|
|
masterKeyDigest := tokenhash.HashToken(rawMasterKey)
|
|
|
|
master := &model.Master{
|
|
Name: name,
|
|
MasterKey: string(hashedMasterKey),
|
|
MasterKeyDigest: masterKeyDigest,
|
|
Group: group,
|
|
MaxChildKeys: maxChildKeys,
|
|
GlobalQPS: globalQPS,
|
|
Status: "active",
|
|
Epoch: 1,
|
|
}
|
|
|
|
if err := s.db.Create(master).Error; err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return master, rawMasterKey, nil
|
|
}
|
|
|
|
func (s *MasterService) ValidateMasterKey(masterKey string) (*model.Master, error) {
|
|
digest := tokenhash.HashToken(masterKey)
|
|
|
|
var master model.Master
|
|
if err := s.db.Where("master_key_digest = ?", digest).First(&master).Error; err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
// Backward compatibility: look for legacy rows without digest.
|
|
var masters []model.Master
|
|
if err := s.db.Where("master_key_digest = '' OR master_key_digest IS NULL").Find(&masters).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, m := range masters {
|
|
if bcrypt.CompareHashAndPassword([]byte(m.MasterKey), []byte(masterKey)) == nil {
|
|
master = m
|
|
// Opportunistically backfill digest for next time.
|
|
if strings.TrimSpace(m.MasterKeyDigest) == "" {
|
|
_ = s.db.Model(&m).Update("master_key_digest", digest).Error
|
|
}
|
|
goto verified
|
|
}
|
|
}
|
|
return nil, errors.New("invalid master key")
|
|
}
|
|
|
|
if bcrypt.CompareHashAndPassword([]byte(master.MasterKey), []byte(masterKey)) != nil {
|
|
return nil, errors.New("invalid master key")
|
|
}
|
|
|
|
verified:
|
|
if master.Status != "active" {
|
|
return nil, fmt.Errorf("master is not active")
|
|
}
|
|
|
|
return &master, nil
|
|
}
|
|
|
|
func (s *MasterService) IssueChildKey(masterID uint, group string, scopes string) (*model.Key, string, error) {
|
|
var master model.Master
|
|
if err := s.db.First(&master, masterID).Error; err != nil {
|
|
return nil, "", fmt.Errorf("master not found: %w", err)
|
|
}
|
|
|
|
var count int64
|
|
s.db.Model(&model.Key{}).Where("master_id = ?", masterID).Count(&count)
|
|
if count >= int64(master.MaxChildKeys) {
|
|
return nil, "", fmt.Errorf("child key limit reached for master %d", masterID)
|
|
}
|
|
|
|
rawChildKey, err := generateRandomKey(32)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to generate child key: %w", err)
|
|
}
|
|
|
|
tokenHash := tokenhash.HashToken(rawChildKey)
|
|
|
|
hashedChildKey, err := bcrypt.GenerateFromPassword([]byte(rawChildKey), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to hash child key: %w", err)
|
|
}
|
|
|
|
key := &model.Key{
|
|
MasterID: masterID,
|
|
KeySecret: string(hashedChildKey),
|
|
TokenHash: tokenHash,
|
|
Group: group,
|
|
Scopes: scopes,
|
|
IssuedAtEpoch: master.Epoch,
|
|
Status: "active",
|
|
}
|
|
|
|
if err := s.db.Create(key).Error; err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return key, rawChildKey, nil
|
|
}
|
|
|
|
func generateRandomKey(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|