174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
// 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{}); 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
|
|
}
|