// Package store 负责 SQLite 初始化、敏感字段 AES-GCM 加密存储 package store import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "github.com/glebarez/sqlite" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" gormlogger "gorm.io/gorm/logger" "vpn-platform/backend/internal/model" ) var DB *gorm.DB var gcm cipher.AEAD // Init 打开数据库、迁移、初始化加密密钥与默认账号 func Init(dataDir string) error { if err := os.MkdirAll(dataDir, 0o700); err != nil { return err } // WAL + busy_timeout:避免 monitor 周期写与 API 写并发时 database is locked dsn := filepath.Join(dataDir, "vpn-platform.db") + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ Logger: gormlogger.Default.LogMode(gormlogger.Warn), }) if err != nil { return fmt.Errorf("打开数据库失败: %w", err) } if sqlDB, err := db.DB(); err == nil { sqlDB.SetMaxOpenConns(1) // SQLite 单写者 } // 兼容旧库:VPNUser 唯一约束曾是 username 单列全局唯一 db.Exec("DROP INDEX IF EXISTS idx_vpn_user") if err := db.AutoMigrate(&model.User{}, &model.LDAPConfig{}, &model.MFADevice{}, &model.VPNUser{}, &model.Tunnel{}, &model.Node{}, &model.Plugin{}, &model.AuditLog{}, &model.VPNSession{}, &model.ConnectionLog{}, &model.TrafficPolicy{}, &model.TrafficSample{}, &model.ClientDevice{}, &model.ClientSession{}, &model.IdentityProvider{}, &model.IdentityGrant{}, &model.IdentityProfile{}, &model.FeishuTunnelGrant{}); err != nil { return err } DB = db if err := initKey(dataDir); err != nil { return err } // 默认管理员 admin / admin123(首次登录后强制改密) // Logan.Li var count int64 DB.Model(&model.User{}).Count(&count) if count == 0 { hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) DB.Create(&model.User{Username: "admin", Password: string(hash), Role: "admin", Source: "local", Enabled: true, MustChangePassword: true}) } else { // 兼容早期单管理员数据:管理员升级后保留完整管理权限。 DB.Model(&model.User{}).Where("username = ? AND source = ?", "admin", "local").Update("role", "admin") } // 内置本机节点 var nodeCount int64 DB.Model(&model.Node{}).Count(&nodeCount) if nodeCount == 0 { DB.Create(&model.Node{Name: "本机节点", Address: "127.0.0.1", Port: 22, Kind: "local", Status: "verified", Remark: "平台所在服务器"}) } return nil } func initKey(dataDir string) error { keyFile := filepath.Join(dataDir, "secret.key") raw, err := os.ReadFile(keyFile) if err != nil { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return err } raw = []byte(hex.EncodeToString(key)) if err := os.WriteFile(keyFile, raw, 0o600); err != nil { return err } } keyBytes, err := hex.DecodeString(string(raw)) if err != nil || len(keyBytes) != 32 { sum := sha256.Sum256(raw) keyBytes = sum[:] } block, err := aes.NewCipher(keyBytes) if err != nil { return err } g, err := cipher.NewGCM(block) if err != nil { return err } gcm = g return nil } // EncryptSecret 加密敏感字符串 func EncryptSecret(plain string) (string, error) { if plain == "" { return "", nil } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", err } out := gcm.Seal(nonce, nonce, []byte(plain), nil) return base64.StdEncoding.EncodeToString(out), nil } // DecryptSecret 解密敏感字符串 func DecryptSecret(enc string) (string, error) { if enc == "" { return "", nil } raw, err := base64.StdEncoding.DecodeString(enc) if err != nil { return "", err } if len(raw) < gcm.NonceSize() { return "", fmt.Errorf("密文长度非法") } plain, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil) if err != nil { return "", err } return string(plain), nil } // Audit 写入操作审计 func Audit(username, action, target, detail string) { if DB != nil { DB.Create(&model.AuditLog{Username: username, Action: action, Target: target, Detail: detail}) } } // GetTunnel 按 ID 查询专线 func GetTunnel(id uint) (*model.Tunnel, error) { var t model.Tunnel if err := DB.Preload("Node").First(&t, id).Error; err != nil { return nil, err } return &t, nil } // TunnelConfig 解析专线 Config JSON func TunnelConfig(t *model.Tunnel) (map[string]interface{}, error) { cfg := map[string]interface{}{} if t.Config != "" { if err := json.Unmarshal([]byte(t.Config), &cfg); err != nil { return nil, err } } return cfg, nil } // TunnelSecrets 解析并解密敏感字段 func TunnelSecrets(t *model.Tunnel) (map[string]interface{}, error) { out := map[string]interface{}{} plain, err := DecryptSecret(t.Secrets) if err != nil || plain == "" { return out, nil } _ = json.Unmarshal([]byte(plain), &out) return out, nil }