Files
MyGoNavi/internal/app/global_proxy_persistence.go
Syngnat c7cf9526de 🐛 fix(security): 修复 macOS 无法打开应用及三平台依赖系统钥匙串的问题
- 密文存储:新增 dailysecret 本地存储引擎,连接/代理/AI 密钥不再依赖系统钥匙串
- 启动迁移:自动将已有钥匙串密文迁移到本地 JSON,用户无感知
- WebKit 迁移:从旧版 Wails WebKit LocalStorage 中恢复连接与代理数据
- DMG 修复:移除 --sandbox-safe 避免扩展属性污染签名,新增 xattr 清理与签名校验
- 安全适配:钥匙串不可用时标记完成而非回滚,消除无钥匙串环境下的阻塞
- 出口脱敏:所有连接/代理 API 返回前统一 sanitize 防止密文泄漏
2026-04-13 12:40:25 +08:00

217 lines
5.8 KiB
Go

package app
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/logger"
"GoNavi-Wails/internal/secretstore"
)
const (
globalProxyFileName = "global_proxy.json"
globalProxySecretKind = "global-proxy"
globalProxySecretID = "default"
)
type globalProxySecretBundle struct {
Password string `json:"password,omitempty"`
}
func globalProxyMetadataPath(configDir string) string {
return filepath.Join(configDir, globalProxyFileName)
}
func (a *App) saveGlobalProxy(input connection.SaveGlobalProxyInput) (connection.GlobalProxyView, error) {
if strings.TrimSpace(a.configDir) == "" {
a.configDir = resolveAppConfigDir()
}
existing, err := a.loadStoredGlobalProxyView()
if err != nil && !os.IsNotExist(err) {
return connection.GlobalProxyView{}, err
}
view := connection.GlobalProxyView{
Enabled: input.Enabled,
Type: strings.TrimSpace(input.Type),
Host: strings.TrimSpace(input.Host),
Port: input.Port,
User: strings.TrimSpace(input.User),
}
bundle := globalProxySecretBundle{}
if strings.TrimSpace(input.Password) != "" {
bundle.Password = input.Password
} else if existing.HasPassword {
existingBundle, loadErr := a.loadGlobalProxySecretBundle(existing)
if loadErr != nil {
return connection.GlobalProxyView{}, loadErr
}
bundle = existingBundle
}
if !view.Enabled {
if deleteErr := a.dailySecretStore().DeleteGlobalProxy(); deleteErr != nil {
return connection.GlobalProxyView{}, deleteErr
}
view = connection.GlobalProxyView{Enabled: false}
if err := a.persistGlobalProxyView(view); err != nil {
return connection.GlobalProxyView{}, err
}
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
return connection.GlobalProxyView{}, err
}
return view, nil
}
if strings.TrimSpace(bundle.Password) != "" {
if storeErr := a.dailySecretStore().PutGlobalProxy(toDailyGlobalProxyBundle(bundle)); storeErr != nil {
return connection.GlobalProxyView{}, storeErr
}
view.HasPassword = true
} else {
if deleteErr := a.dailySecretStore().DeleteGlobalProxy(); deleteErr != nil {
return connection.GlobalProxyView{}, deleteErr
}
view.HasPassword = false
}
view.SecretRef = ""
view.Password = ""
if err := a.persistGlobalProxyView(view); err != nil {
return connection.GlobalProxyView{}, err
}
if _, err := setGlobalProxyConfig(true, connection.ProxyConfig{
Type: view.Type,
Host: view.Host,
Port: view.Port,
User: view.User,
Password: bundle.Password,
}); err != nil {
return connection.GlobalProxyView{}, err
}
return sanitizeGlobalProxyView(view), nil
}
func (a *App) persistGlobalProxyView(view connection.GlobalProxyView) error {
if err := os.MkdirAll(a.configDir, 0o755); err != nil {
return err
}
payload, err := json.MarshalIndent(view, "", " ")
if err != nil {
return err
}
return os.WriteFile(globalProxyMetadataPath(a.configDir), payload, 0o644)
}
func (a *App) loadStoredGlobalProxyView() (connection.GlobalProxyView, error) {
data, err := os.ReadFile(globalProxyMetadataPath(a.configDir))
if err != nil {
return connection.GlobalProxyView{}, err
}
var view connection.GlobalProxyView
if err := json.Unmarshal(data, &view); err != nil {
return connection.GlobalProxyView{}, err
}
return view, nil
}
func (a *App) loadGlobalProxySecretBundle(view connection.GlobalProxyView) (globalProxySecretBundle, error) {
inline := extractGlobalProxySecretBundle(view)
if strings.TrimSpace(inline.Password) != "" {
return inline, nil
}
if !view.HasPassword {
return globalProxySecretBundle{}, nil
}
bundle, ok, err := a.dailySecretStore().GetGlobalProxy()
if err != nil {
return globalProxySecretBundle{}, err
}
if ok {
return fromDailyGlobalProxyBundle(bundle), nil
}
return globalProxySecretBundle{}, os.ErrNotExist
}
func (a *App) loadGlobalProxySecretBundleFromStore(view connection.GlobalProxyView) (globalProxySecretBundle, error) {
if a.secretStore == nil {
return globalProxySecretBundle{}, fmt.Errorf("secret store unavailable")
}
ref := strings.TrimSpace(view.SecretRef)
if ref == "" {
var err error
ref, err = secretstore.BuildRef(globalProxySecretKind, globalProxySecretID)
if err != nil {
return globalProxySecretBundle{}, err
}
}
payload, err := a.secretStore.Get(ref)
if err != nil {
return globalProxySecretBundle{}, err
}
var bundle globalProxySecretBundle
if err := json.Unmarshal(payload, &bundle); err != nil {
return globalProxySecretBundle{}, err
}
return bundle, nil
}
func (a *App) storeGlobalProxySecret(existingRef string, bundle globalProxySecretBundle) (string, error) {
if a.secretStore == nil {
return "", fmt.Errorf("secret store unavailable")
}
if err := a.secretStore.HealthCheck(); err != nil {
return "", err
}
ref := strings.TrimSpace(existingRef)
if ref == "" {
var err error
ref, err = secretstore.BuildRef(globalProxySecretKind, globalProxySecretID)
if err != nil {
return "", err
}
}
payload, err := json.Marshal(bundle)
if err != nil {
return "", err
}
if err := a.secretStore.Put(ref, payload); err != nil {
return "", err
}
return ref, nil
}
func (a *App) loadPersistedGlobalProxy() {
view, err := a.loadStoredGlobalProxyView()
if err != nil {
if !os.IsNotExist(err) {
logger.Error(err, "加载全局代理元数据失败")
}
return
}
proxyConfig := connection.ProxyConfig{
Type: view.Type,
Host: view.Host,
Port: view.Port,
User: view.User,
}
if view.HasPassword {
bundle, loadErr := a.loadGlobalProxySecretBundle(view)
if loadErr != nil {
logger.Error(loadErr, "加载全局代理密码失败")
return
}
proxyConfig.Password = bundle.Password
}
if _, err := setGlobalProxyConfig(view.Enabled, proxyConfig); err != nil {
logger.Error(err, "恢复全局代理配置失败")
}
}