// Package byok 用户自带密钥:AES-GCM 加密存储与脱敏呈现(shenshi-gateway.md §6)。 // // 密钥分层:平台密钥走端点目录(SHENSHI_ENDPOINTS),用户 BYOK 走本包; // 主密钥由 SHENSHI_BYOK_MASTER_KEY 提供(32 字节 hex/base64);未配置时 // BYOK 功能整体禁用(fail-closed,HJ-2003),不留明文降级路径。 package byok import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "strings" "time" "hunjing/shenshi/internal/store" ) // Masked API 呈现形态:永不回明文。 type Masked struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` BaseURL string `json:"base_url"` Model string `json:"model"` CreatedAt time.Time `json:"created_at"` LastUsed *time.Time `json:"last_used,omitempty"` // APIKeyMask 形如 sk-****abcd(前缀仅示意类型,不泄露中段) APIKeyMask string `json:"api_key_mask"` } // Vault BYOK 密钥库。 type Vault struct { aesgcm cipher.AEAD store store.Store } // NewVault 构造;masterKey 为 32 字节 hex 或 base64。 func NewVault(masterKey string, st store.Store) (*Vault, error) { if masterKey == "" { return nil, fmt.Errorf("SHENSHI_BYOK_MASTER_KEY 未配置,BYOK 功能禁用") } key, err := decodeKey(masterKey) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } return &Vault{aesgcm: gcm, store: st}, nil } func decodeKey(raw string) ([]byte, error) { raw = strings.TrimSpace(raw) if b, err := hex.DecodeString(raw); err == nil && len(b) == 32 { return b, nil } if b, err := base64.StdEncoding.DecodeString(raw); err == nil && len(b) == 32 { return b, nil } return nil, fmt.Errorf("SHENSHI_BYOK_MASTER_KEY 须为 32 字节 hex 或 base64") } func (v *Vault) encrypt(plaintext string) (string, error) { nonce := make([]byte, v.aesgcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", err } sealed := v.aesgcm.Seal(nil, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(append(nonce, sealed...)), nil } func (v *Vault) decrypt(encoded string) (string, error) { raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", err } n := v.aesgcm.NonceSize() if len(raw) <= n { return "", fmt.Errorf("密文形态非法") } plain, err := v.aesgcm.Open(nil, raw[:n], raw[n:], nil) if err != nil { return "", fmt.Errorf("HJ-2003 BYOK 密钥解密失败") } return string(plain), nil } // Register 登记用户密钥,返回脱敏形态。 func (v *Vault) Register(userID, name, typ, baseURL, apiKey, model string) (*Masked, error) { enc, err := v.encrypt(apiKey) if err != nil { return nil, err } key := store.ByokKey{ ID: newID(), UserID: userID, Name: name, Type: typ, BaseURL: baseURL, APIKey: enc, Model: model, CreatedAt: time.Now().UTC(), } if err := v.store.UpsertByok(key); err != nil { return nil, err } m := mask(key, apiKey) return &m, nil } // List 脱敏列举用户有效密钥。 func (v *Vault) List(userID string) ([]Masked, error) { keys, err := v.store.ListByok(userID) if err != nil { return nil, err } out := make([]Masked, 0, len(keys)) for _, k := range keys { out = append(out, mask(k, "")) } return out, nil } // Resolve 取解密后的密钥材料(路由期专用,结果不落日志)。 func (v *Vault) Resolve(userID, id string) (*store.ByokKey, string, error) { k, err := v.store.GetByok(userID, id) if err != nil || k == nil { return nil, "", fmt.Errorf("HJ-2003 BYOK 密钥不存在或已撤回") } plain, err := v.decrypt(k.APIKey) if err != nil { return nil, "", err } now := time.Now().UTC() _ = v.store.TouchByok(id, now) k.LastUsed = &now return k, plain, nil } // Revoke 撤回(软删,立即失效)。 func (v *Vault) Revoke(userID, id string) error { return v.store.RevokeByok(userID, id) } func mask(k store.ByokKey, plainForPrefix string) Masked { prefix := "" if plainForPrefix != "" { prefix = plainForPrefix[:min(3, len(plainForPrefix))] } m := Masked{ ID: k.ID, Name: k.Name, Type: k.Type, BaseURL: k.BaseURL, Model: k.Model, CreatedAt: k.CreatedAt, LastUsed: k.LastUsed, APIKeyMask: prefix + "****", } return m } func newID() string { b := make([]byte, 10) _, _ = rand.Read(b) return "byok_" + hex.EncodeToString(b) }