mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 06:22:57 +08:00
- 密文存储:新增 dailysecret 本地存储引擎,连接/代理/AI 密钥不再依赖系统钥匙串 - 启动迁移:自动将已有钥匙串密文迁移到本地 JSON,用户无感知 - WebKit 迁移:从旧版 Wails WebKit LocalStorage 中恢复连接与代理数据 - DMG 修复:移除 --sandbox-safe 避免扩展属性污染签名,新增 xattr 清理与签名校验 - 安全适配:钥匙串不可用时标记完成而非回滚,消除无钥匙串环境下的阻塞 - 出口脱敏:所有连接/代理 API 返回前统一 sanitize 防止密文泄漏
236 lines
5.7 KiB
Go
236 lines
5.7 KiB
Go
package dailysecret
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
fileName = "daily_secrets.json"
|
|
schemaVersion = 1
|
|
)
|
|
|
|
type ConnectionBundle struct {
|
|
Password string `json:"password,omitempty"`
|
|
SSHPassword string `json:"sshPassword,omitempty"`
|
|
ProxyPassword string `json:"proxyPassword,omitempty"`
|
|
HTTPTunnelPassword string `json:"httpTunnelPassword,omitempty"`
|
|
MySQLReplicaPassword string `json:"mysqlReplicaPassword,omitempty"`
|
|
MongoReplicaPassword string `json:"mongoReplicaPassword,omitempty"`
|
|
OpaqueURI string `json:"opaqueURI,omitempty"`
|
|
OpaqueDSN string `json:"opaqueDSN,omitempty"`
|
|
}
|
|
|
|
func (b ConnectionBundle) HasAny() bool {
|
|
return strings.TrimSpace(b.Password) != "" ||
|
|
strings.TrimSpace(b.SSHPassword) != "" ||
|
|
strings.TrimSpace(b.ProxyPassword) != "" ||
|
|
strings.TrimSpace(b.HTTPTunnelPassword) != "" ||
|
|
strings.TrimSpace(b.MySQLReplicaPassword) != "" ||
|
|
strings.TrimSpace(b.MongoReplicaPassword) != "" ||
|
|
strings.TrimSpace(b.OpaqueURI) != "" ||
|
|
strings.TrimSpace(b.OpaqueDSN) != ""
|
|
}
|
|
|
|
type GlobalProxyBundle struct {
|
|
Password string `json:"password,omitempty"`
|
|
}
|
|
|
|
func (b GlobalProxyBundle) HasAny() bool {
|
|
return strings.TrimSpace(b.Password) != ""
|
|
}
|
|
|
|
type ProviderBundle struct {
|
|
APIKey string `json:"apiKey,omitempty"`
|
|
SensitiveHeaders map[string]string `json:"sensitiveHeaders,omitempty"`
|
|
}
|
|
|
|
func (b ProviderBundle) HasAny() bool {
|
|
return strings.TrimSpace(b.APIKey) != "" || len(b.SensitiveHeaders) > 0
|
|
}
|
|
|
|
type File struct {
|
|
SchemaVersion int `json:"schemaVersion,omitempty"`
|
|
Connections map[string]ConnectionBundle `json:"connections,omitempty"`
|
|
GlobalProxy *GlobalProxyBundle `json:"globalProxy,omitempty"`
|
|
AIProviders map[string]ProviderBundle `json:"aiProviders,omitempty"`
|
|
}
|
|
|
|
type Store struct {
|
|
root string
|
|
}
|
|
|
|
func NewStore(root string) *Store {
|
|
return &Store{root: strings.TrimSpace(root)}
|
|
}
|
|
|
|
func (s *Store) Path() string {
|
|
return filepath.Join(s.root, fileName)
|
|
}
|
|
|
|
func (s *Store) Load() (File, error) {
|
|
if strings.TrimSpace(s.root) == "" {
|
|
return File{SchemaVersion: schemaVersion}, nil
|
|
}
|
|
data, err := os.ReadFile(s.Path())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return File{SchemaVersion: schemaVersion}, nil
|
|
}
|
|
return File{}, err
|
|
}
|
|
var file File
|
|
if err := json.Unmarshal(data, &file); err != nil {
|
|
return File{}, err
|
|
}
|
|
if file.SchemaVersion == 0 {
|
|
file.SchemaVersion = schemaVersion
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (s *Store) Save(file File) error {
|
|
if strings.TrimSpace(s.root) == "" {
|
|
return nil
|
|
}
|
|
file.SchemaVersion = schemaVersion
|
|
if len(file.Connections) == 0 {
|
|
file.Connections = nil
|
|
}
|
|
if file.GlobalProxy != nil && !file.GlobalProxy.HasAny() {
|
|
file.GlobalProxy = nil
|
|
}
|
|
if len(file.AIProviders) == 0 {
|
|
file.AIProviders = nil
|
|
}
|
|
if err := os.MkdirAll(s.root, 0o755); err != nil {
|
|
return err
|
|
}
|
|
payload, err := json.MarshalIndent(file, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(s.Path(), payload, 0o644)
|
|
}
|
|
|
|
func (s *Store) GetConnection(id string) (ConnectionBundle, bool, error) {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return ConnectionBundle{}, false, err
|
|
}
|
|
bundle, ok := file.Connections[strings.TrimSpace(id)]
|
|
return bundle, ok, nil
|
|
}
|
|
|
|
func (s *Store) PutConnection(id string, bundle ConnectionBundle) error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bundle.HasAny() {
|
|
return s.deleteConnectionFromFile(file, id)
|
|
}
|
|
if file.Connections == nil {
|
|
file.Connections = make(map[string]ConnectionBundle)
|
|
}
|
|
file.Connections[strings.TrimSpace(id)] = bundle
|
|
return s.Save(file)
|
|
}
|
|
|
|
func (s *Store) DeleteConnection(id string) error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.deleteConnectionFromFile(file, id)
|
|
}
|
|
|
|
func (s *Store) deleteConnectionFromFile(file File, id string) error {
|
|
if len(file.Connections) != 0 {
|
|
delete(file.Connections, strings.TrimSpace(id))
|
|
}
|
|
return s.Save(file)
|
|
}
|
|
|
|
func (s *Store) GetGlobalProxy() (GlobalProxyBundle, bool, error) {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return GlobalProxyBundle{}, false, err
|
|
}
|
|
if file.GlobalProxy == nil {
|
|
return GlobalProxyBundle{}, false, nil
|
|
}
|
|
return *file.GlobalProxy, true, nil
|
|
}
|
|
|
|
func (s *Store) PutGlobalProxy(bundle GlobalProxyBundle) error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bundle.HasAny() {
|
|
file.GlobalProxy = nil
|
|
return s.Save(file)
|
|
}
|
|
copyBundle := bundle
|
|
file.GlobalProxy = ©Bundle
|
|
return s.Save(file)
|
|
}
|
|
|
|
func (s *Store) DeleteGlobalProxy() error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file.GlobalProxy = nil
|
|
return s.Save(file)
|
|
}
|
|
|
|
func (s *Store) GetAIProvider(id string) (ProviderBundle, bool, error) {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return ProviderBundle{}, false, err
|
|
}
|
|
bundle, ok := file.AIProviders[strings.TrimSpace(id)]
|
|
return bundle, ok, nil
|
|
}
|
|
|
|
func (s *Store) PutAIProvider(id string, bundle ProviderBundle) error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bundle.HasAny() {
|
|
return s.deleteAIProviderFromFile(file, id)
|
|
}
|
|
if file.AIProviders == nil {
|
|
file.AIProviders = make(map[string]ProviderBundle)
|
|
}
|
|
if len(bundle.SensitiveHeaders) > 0 {
|
|
cloned := make(map[string]string, len(bundle.SensitiveHeaders))
|
|
for key, value := range bundle.SensitiveHeaders {
|
|
cloned[key] = value
|
|
}
|
|
bundle.SensitiveHeaders = cloned
|
|
}
|
|
file.AIProviders[strings.TrimSpace(id)] = bundle
|
|
return s.Save(file)
|
|
}
|
|
|
|
func (s *Store) DeleteAIProvider(id string) error {
|
|
file, err := s.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.deleteAIProviderFromFile(file, id)
|
|
}
|
|
|
|
func (s *Store) deleteAIProviderFromFile(file File, id string) error {
|
|
if len(file.AIProviders) != 0 {
|
|
delete(file.AIProviders, strings.TrimSpace(id))
|
|
}
|
|
return s.Save(file)
|
|
}
|