Files
MyGoNavi/internal/app/connection_secret_resolution.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

154 lines
5.0 KiB
Go

package app
import (
"errors"
"fmt"
"os"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/secretstore"
)
func (a *App) resolveConnectionSecrets(config connection.ConnectionConfig) (connection.ConnectionConfig, error) {
if strings.TrimSpace(config.ID) == "" {
return config, nil
}
repo := newSavedConnectionRepository(a.configDir, a.secretStore)
view, err := repo.Find(config.ID)
if err != nil {
if shouldFallbackToInlineConnectionSecrets(config, err) {
return config, nil
}
return config, normalizeConnectionSecretResolutionError(config, err)
}
base := config
if connectionMetadataLooksEmpty(base) {
base = view.Config
}
bundle, err := repo.loadSecretBundle(view)
if err != nil {
if shouldFallbackToInlineConnectionSecrets(config, err) {
return mergeInlineConnectionSecrets(base, config), nil
}
return base, normalizeConnectionSecretResolutionError(base, err)
}
resolved := mergeConnectionSecretBundleIntoConfig(base, bundle)
resolved.ID = view.ID
return resolved, nil
}
func shouldFallbackToInlineConnectionSecrets(config connection.ConnectionConfig, err error) bool {
if err == nil || !connectionConfigCarriesInlineSecrets(config) || secretstore.IsUnavailable(err) {
return false
}
if errors.Is(err, os.ErrNotExist) {
return true
}
lower := strings.ToLower(strings.TrimSpace(err.Error()))
return strings.Contains(lower, "saved connection not found:")
}
func connectionConfigCarriesInlineSecrets(config connection.ConnectionConfig) bool {
return strings.TrimSpace(config.Password) != "" ||
strings.TrimSpace(config.SSH.Password) != "" ||
strings.TrimSpace(config.Proxy.Password) != "" ||
strings.TrimSpace(config.HTTPTunnel.Password) != "" ||
strings.TrimSpace(config.MySQLReplicaPassword) != "" ||
strings.TrimSpace(config.MongoReplicaPassword) != "" ||
strings.TrimSpace(config.URI) != "" ||
strings.TrimSpace(config.DSN) != ""
}
func mergeInlineConnectionSecrets(base connection.ConnectionConfig, inline connection.ConnectionConfig) connection.ConnectionConfig {
merged := base
if strings.TrimSpace(inline.Password) != "" {
merged.Password = inline.Password
}
if strings.TrimSpace(inline.SSH.Password) != "" {
merged.SSH.Password = inline.SSH.Password
}
if strings.TrimSpace(inline.Proxy.Password) != "" {
merged.Proxy.Password = inline.Proxy.Password
}
if strings.TrimSpace(inline.HTTPTunnel.Password) != "" {
merged.HTTPTunnel.Password = inline.HTTPTunnel.Password
}
if strings.TrimSpace(inline.MySQLReplicaPassword) != "" {
merged.MySQLReplicaPassword = inline.MySQLReplicaPassword
}
if strings.TrimSpace(inline.MongoReplicaPassword) != "" {
merged.MongoReplicaPassword = inline.MongoReplicaPassword
}
if strings.TrimSpace(inline.URI) != "" {
merged.URI = inline.URI
}
if strings.TrimSpace(inline.DSN) != "" {
merged.DSN = inline.DSN
}
return merged
}
func normalizeConnectionSecretResolutionError(config connection.ConnectionConfig, err error) error {
if err == nil {
return nil
}
lower := strings.ToLower(strings.TrimSpace(err.Error()))
switch {
case strings.Contains(lower, "saved connection not found:"):
if connectionMetadataLooksEmpty(config) {
return fmt.Errorf("未找到已保存连接,可能已被删除,请刷新后重试")
}
return fmt.Errorf("未找到当前连接对应的已保存密文,请重新填写密码并保存后再试")
case errors.Is(err, os.ErrNotExist):
return fmt.Errorf("未找到当前连接对应的已保存密文,请重新填写密码并保存后再试")
case strings.Contains(lower, "secret store unavailable"):
return fmt.Errorf("系统密文存储当前不可用,请检查系统钥匙串或凭据管理器后再试")
default:
return err
}
}
func connectionMetadataLooksEmpty(config connection.ConnectionConfig) bool {
return strings.TrimSpace(config.Type) == "" &&
strings.TrimSpace(config.Host) == "" &&
config.Port == 0 &&
strings.TrimSpace(config.User) == "" &&
strings.TrimSpace(config.Database) == "" &&
strings.TrimSpace(config.DSN) == "" &&
strings.TrimSpace(config.URI) == "" &&
len(config.Hosts) == 0
}
func mergeConnectionSecretBundleIntoConfig(config connection.ConnectionConfig, bundle connectionSecretBundle) connection.ConnectionConfig {
merged := config
if strings.TrimSpace(merged.Password) == "" {
merged.Password = bundle.Password
}
if strings.TrimSpace(merged.SSH.Password) == "" {
merged.SSH.Password = bundle.SSHPassword
}
if strings.TrimSpace(merged.Proxy.Password) == "" {
merged.Proxy.Password = bundle.ProxyPassword
}
if strings.TrimSpace(merged.HTTPTunnel.Password) == "" {
merged.HTTPTunnel.Password = bundle.HTTPTunnelPassword
}
if strings.TrimSpace(merged.MySQLReplicaPassword) == "" {
merged.MySQLReplicaPassword = bundle.MySQLReplicaPassword
}
if strings.TrimSpace(merged.MongoReplicaPassword) == "" {
merged.MongoReplicaPassword = bundle.MongoReplicaPassword
}
if strings.TrimSpace(merged.URI) == "" {
merged.URI = bundle.OpaqueURI
}
if strings.TrimSpace(merged.DSN) == "" {
merged.DSN = bundle.OpaqueDSN
}
return merged
}