mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
feat(secrets): encrypt sensitive store fields (AES-GCM, master.key 0600, api_key_env) + fast models-endpoint probing (fix zen backlog + source status) + UI cleanup (drop redundant parens labels, grid models 4/row)
This commit is contained in:
@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
@ -12,10 +13,25 @@ type Store struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
data RuntimeConfig
|
||||
box *SecretBox
|
||||
}
|
||||
|
||||
func NewStore(path string) *Store {
|
||||
return &Store{path: path}
|
||||
s := &Store{path: path}
|
||||
if box, err := NewSecretBox(path); err == nil {
|
||||
s.box = box
|
||||
} else {
|
||||
log.Printf("[config] secrets disabled: %v (sensitive values will be stored in plaintext)", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SecretBox exposes the box used for persist-time encryption of source
|
||||
// api_key / header values read from other places (e.g. YAML). May be nil.
|
||||
func (s *Store) SecretBox() *SecretBox {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.box
|
||||
}
|
||||
|
||||
// Load reads the runtime file (missing file = empty state).
|
||||
@ -30,7 +46,27 @@ func (s *Store) Load() error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &s.data)
|
||||
if err := json.Unmarshal(data, &s.data); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.box != nil {
|
||||
s.decryptLocked()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decryptLocked replaces persisted ciphertext fields with their plaintext in
|
||||
// memory (the runtime always works with plaintext; only the file is sealed).
|
||||
func (s *Store) decryptLocked() {
|
||||
for i := range s.data.Sources {
|
||||
s.data.Sources[i].APIKey = s.box.MustDecrypt(s.data.Sources[i].APIKey)
|
||||
for k, v := range s.data.Sources[i].Headers {
|
||||
s.data.Sources[i].Headers[k] = s.box.MustDecrypt(v)
|
||||
}
|
||||
}
|
||||
for i := range s.data.Keys {
|
||||
s.data.Keys[i].Key = s.box.MustDecrypt(s.data.Keys[i].Key)
|
||||
}
|
||||
}
|
||||
|
||||
// List returns the runtime sources (those edited via web UI).
|
||||
@ -115,11 +151,44 @@ func (s *Store) DeletedAdapters() map[string]bool {
|
||||
}
|
||||
|
||||
func (s *Store) persistLocked() error {
|
||||
if s.box != nil {
|
||||
s.encryptLocked()
|
||||
}
|
||||
b, err := json.MarshalIndent(s.data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, b, 0644)
|
||||
if s.box != nil {
|
||||
s.decryptLocked()
|
||||
}
|
||||
if err := os.WriteFile(s.path, b, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encryptLocked seals sensitive fields for the write; decryptLocked runs
|
||||
// right after marshaling so in-memory data stays plaintext.
|
||||
func (s *Store) encryptLocked() {
|
||||
for i := range s.data.Sources {
|
||||
if v, err := s.box.Encrypt(s.data.Sources[i].APIKey); err == nil {
|
||||
s.data.Sources[i].APIKey = v
|
||||
}
|
||||
h := s.data.Sources[i].Headers
|
||||
for k, v := range h {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if e, err := s.box.Encrypt(v); err == nil {
|
||||
h[k] = e
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range s.data.Keys {
|
||||
if v, err := s.box.Encrypt(s.data.Keys[i].Key); err == nil {
|
||||
s.data.Keys[i].Key = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containsString(list []string, s string) bool {
|
||||
|
||||
Reference in New Issue
Block a user