Files
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

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 = &copyBundle
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)
}