diff --git a/.env.example b/.env.example index 50ad92dd..dc269f1b 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,9 @@ REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD=redis123456 +# 数据加密密钥 +DATA_ENCRYPTION_KEY=my_secret_encryption_key + # Ports Configuration # Backend API server port (internal: 8080, external: configurable) NOFX_BACKEND_PORT=8080 diff --git a/.gitignore b/.gitignore index 9f3bdd5d..a5c1c3c3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,11 @@ config.db certs/ beta_codes.txt +# 密钥文件 +keys/ +*.key +*.pem + # 决策日志 decision_logs/ coin_pool_cache/ diff --git a/api/server.go b/api/server.go index a10a39f6..ca06904c 100644 --- a/api/server.go +++ b/api/server.go @@ -8,6 +8,7 @@ import ( "net/http" "nofx/auth" "nofx/config" + "nofx/crypto" "nofx/decision" "nofx/manager" "nofx/trader" @@ -24,11 +25,12 @@ type Server struct { router *gin.Engine traderManager *manager.TraderManager database config.DatabaseInterface + cryptoService *crypto.CryptoService port int } // NewServer 创建API服务器 -func NewServer(traderManager *manager.TraderManager, database config.DatabaseInterface, port int) *Server { +func NewServer(traderManager *manager.TraderManager, database config.DatabaseInterface, cryptoService *crypto.CryptoService, port int) *Server { // 设置为Release模式(减少日志输出) gin.SetMode(gin.ReleaseMode) @@ -37,10 +39,17 @@ func NewServer(traderManager *manager.TraderManager, database config.DatabaseInt // 启用CORS router.Use(corsMiddleware()) + if cryptoService == nil { + log.Printf("⚠️ 加密服务未初始化,敏感数据加解密功能不可用") + } else { + database.SetCryptoService(cryptoService) + } + s := &Server{ router: router, traderManager: traderManager, database: database, + cryptoService: cryptoService, port: port, } @@ -123,6 +132,7 @@ func (s *Server) setupRoutes() { // 交易所配置 protected.GET("/exchanges", s.handleGetExchangeConfigs) protected.PUT("/exchanges", s.handleUpdateExchangeConfigs) + protected.PUT("/exchanges/encrypted", s.handleUpdateExchangeConfigsEncrypted) // 用户信号源配置 protected.GET("/user/signal-sources", s.handleGetUserSignalSource) @@ -179,11 +189,19 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaMode := betaModeStr == "true" + // 获取RSA公钥 + var rsaPublicKey string + if s.cryptoService != nil { + rsaPublicKey = s.cryptoService.GetPublicKeyPEM() + } + c.JSON(http.StatusOK, gin.H{ "beta_mode": betaMode, "default_coins": defaultCoins, "btc_eth_leverage": btcEthLeverage, "altcoin_leverage": altcoinLeverage, + "rsa_public_key": rsaPublicKey, + "rsa_key_id": "rsa-key-2025-11-05", }) } @@ -1638,8 +1656,10 @@ func (s *Server) handleCompleteRegistration(c *gin.Context) { // handleLogin 处理用户登录请求 func (s *Server) handleLogin(c *gin.Context) { var req struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required"` + Email string `json:"email"` + EmailEncrypted *crypto.EncryptedPayload `json:"email_encrypted"` + Password string `json:"password"` + PasswordEncrypted *crypto.EncryptedPayload `json:"password_encrypted"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -1647,6 +1667,51 @@ func (s *Server) handleLogin(c *gin.Context) { return } + if req.EmailEncrypted != nil { + if s.cryptoService == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "加密服务不可用"}) + return + } + + decryptedEmail, err := s.cryptoService.DecryptSensitiveData(req.EmailEncrypted) + if err != nil { + log.Printf("❌ 登录邮箱解密失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱解密失败"}) + return + } + req.Email = decryptedEmail + } + + if req.PasswordEncrypted != nil { + if s.cryptoService == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "加密服务不可用"}) + return + } + + decryptedPassword, err := s.cryptoService.DecryptSensitiveData(req.PasswordEncrypted) + if err != nil { + log.Printf("❌ 登录密码解密失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "密码解密失败"}) + return + } + req.Password = decryptedPassword + } + + req.Email = strings.TrimSpace(req.Email) + if req.Email == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱不能为空"}) + return + } + if !strings.Contains(req.Email, "@") { + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式错误"}) + return + } + + if strings.TrimSpace(req.Password) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "密码不能为空"}) + return + } + // 获取用户信息 user, err := s.database.GetUserByEmail(req.Email) if err != nil { @@ -2026,3 +2091,64 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } + +// handleUpdateExchangeConfigsEncrypted 更新交易所配置(加密传输) +func (s *Server) handleUpdateExchangeConfigsEncrypted(c *gin.Context) { + if s.cryptoService == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "加密服务不可用"}) + return + } + + userID := c.GetString("user_id") + + // 接收加密载荷 + var payload crypto.EncryptedPayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 解密数据 + decryptedData, err := s.cryptoService.DecryptSensitiveData(&payload) + if err != nil { + log.Printf("❌ 解密失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "解密失败"}) + return + } + + // 解析解密后的数据 + var req UpdateExchangeConfigRequest + if err := json.Unmarshal([]byte(decryptedData), &req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "数据格式错误"}) + return + } + + // 更新每个交易所的配置 + for exchangeID, exchangeData := range req.Exchanges { + err := s.database.UpdateExchange( + userID, + exchangeID, + exchangeData.Enabled, + exchangeData.APIKey, + exchangeData.SecretKey, + exchangeData.Testnet, + exchangeData.HyperliquidWalletAddr, + exchangeData.AsterUser, + exchangeData.AsterSigner, + exchangeData.AsterPrivateKey, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)}) + return + } + } + + // 重新加载该用户的所有交易员,使新配置立即生效 + err = s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err) + } + + log.Printf("✓ 交易所配置已通过加密方式更新") + c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) +} diff --git a/config/database.go b/config/database.go index 51876587..1e6e1504 100644 --- a/config/database.go +++ b/config/database.go @@ -1,126 +1,130 @@ package config import ( - "fmt" - "time" + "fmt" + "time" + + "nofx/crypto" ) // DatabaseInterface 定义了数据库实现需要提供的方法集合 type DatabaseInterface interface { - CreateUser(user *User) error - GetUserByEmail(email string) (*User, error) - GetUserByID(userID string) (*User, error) - GetAllUsers() ([]string, error) - UpdateUserOTPVerified(userID string, verified bool) error - GetAIModels(userID string) ([]*AIModelConfig, error) - UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error - GetExchanges(userID string) ([]*ExchangeConfig, error) - UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error - CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error - CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error - CreateTrader(trader *TraderRecord) error - GetTraders(userID string) ([]*TraderRecord, error) - UpdateTraderStatus(userID, id string, isRunning bool) error - UpdateTrader(trader *TraderRecord) error - UpdateTraderInitialBalance(userID, id string, newBalance float64) error - UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error - DeleteTrader(userID, id string) error - GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) - GetSystemConfig(key string) (string, error) - SetSystemConfig(key, value string) error - CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error - GetUserSignalSource(userID string) (*UserSignalSource, error) - UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error - GetCustomCoins() []string - LoadBetaCodesFromFile(filePath string) error - ValidateBetaCode(code string) (bool, error) - UseBetaCode(code, userEmail string) error - GetBetaCodeStats() (total, used int, err error) - Close() error + SetCryptoService(cs *crypto.CryptoService) + CreateUser(user *User) error + GetUserByEmail(email string) (*User, error) + GetUserByID(userID string) (*User, error) + GetAllUsers() ([]string, error) + UpdateUserOTPVerified(userID string, verified bool) error + GetAIModels(userID string) ([]*AIModelConfig, error) + UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error + GetExchanges(userID string) ([]*ExchangeConfig, error) + UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error + CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error + CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error + CreateTrader(trader *TraderRecord) error + GetTraders(userID string) ([]*TraderRecord, error) + UpdateTraderStatus(userID, id string, isRunning bool) error + UpdateTrader(trader *TraderRecord) error + UpdateTraderInitialBalance(userID, id string, newBalance float64) error + UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error + DeleteTrader(userID, id string) error + GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) + GetSystemConfig(key string) (string, error) + SetSystemConfig(key, value string) error + CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error + GetUserSignalSource(userID string) (*UserSignalSource, error) + UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error + GetCustomCoins() []string + LoadBetaCodesFromFile(filePath string) error + ValidateBetaCode(code string) (bool, error) + UseBetaCode(code, userEmail string) error + GetBetaCodeStats() (total, used int, err error) + Close() error } // User 用户配置 type User struct { - ID string `json:"id"` - Email string `json:"email"` - PasswordHash string `json:"-"` // 不返回到前端 - OTPSecret string `json:"-"` // 不返回到前端 - OTPVerified bool `json:"otp_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"-"` + OTPSecret string `json:"-"` + OTPVerified bool `json:"otp_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // AIModelConfig AI模型配置 type AIModelConfig struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Provider string `json:"provider"` - Enabled bool `json:"enabled"` - APIKey string `json:"apiKey"` - CustomAPIURL string `json:"customApiUrl"` - CustomModelName string `json:"customModelName"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Provider string `json:"provider"` + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + CustomAPIURL string `json:"customApiUrl"` + CustomModelName string `json:"customModelName"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // ExchangeConfig 交易所配置 type ExchangeConfig struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Type string `json:"type"` - Enabled bool `json:"enabled"` - APIKey string `json:"apiKey"` - SecretKey string `json:"secretKey"` - Testnet bool `json:"testnet"` - HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` - AsterUser string `json:"asterUser"` - AsterSigner string `json:"asterSigner"` - AsterPrivateKey string `json:"asterPrivateKey"` - Deleted bool `json:"deleted"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + SecretKey string `json:"secretKey"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` + AsterUser string `json:"asterUser"` + AsterSigner string `json:"asterSigner"` + AsterPrivateKey string `json:"asterPrivateKey"` + DEXWalletPrivateKey string `json:"dexWalletPrivateKey"` // 统一的DEX私钥字段 + Deleted bool `json:"deleted"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -// TraderRecord 交易员配置(数据库实体) +// TraderRecord 交易员配置 type TraderRecord struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - AIModelID string `json:"ai_model_id"` - ExchangeID string `json:"exchange_id"` - InitialBalance float64 `json:"initial_balance"` - ScanIntervalMinutes int `json:"scan_interval_minutes"` - IsRunning bool `json:"is_running"` - BTCETHLeverage int `json:"btc_eth_leverage"` - AltcoinLeverage int `json:"altcoin_leverage"` - TradingSymbols string `json:"trading_symbols"` - UseCoinPool bool `json:"use_coin_pool"` - UseOITop bool `json:"use_oi_top"` - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_base_prompt"` - SystemPromptTemplate string `json:"system_prompt_template"` - IsCrossMargin bool `json:"is_cross_margin"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + AIModelID string `json:"ai_model_id"` + ExchangeID string `json:"exchange_id"` + InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` + IsRunning bool `json:"is_running"` + BTCETHLeverage int `json:"btc_eth_leverage"` + AltcoinLeverage int `json:"altcoin_leverage"` + TradingSymbols string `json:"trading_symbols"` + UseCoinPool bool `json:"use_coin_pool"` + UseOITop bool `json:"use_oi_top"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + SystemPromptTemplate string `json:"system_prompt_template"` + IsCrossMargin bool `json:"is_cross_margin"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // UserSignalSource 用户信号源配置 type UserSignalSource struct { - ID int `json:"id"` - UserID string `json:"user_id"` - CoinPoolURL string `json:"coin_pool_url"` - OITopURL string `json:"oi_top_url"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id"` + UserID string `json:"user_id"` + CoinPoolURL string `json:"coin_pool_url"` + OITopURL string `json:"oi_top_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // NewDatabase 创建数据库连接(仅支持 PostgreSQL) func NewDatabase() (DatabaseInterface, error) { - pgDB, err := NewPostgreSQLDatabase() - if err != nil { - return nil, fmt.Errorf("创建PostgreSQL数据库失败: %w", err) - } - return pgDB, nil + pgDB, err := NewPostgreSQLDatabase() + if err != nil { + return nil, fmt.Errorf("创建PostgreSQL数据库失败: %w", err) + } + return pgDB, nil } diff --git a/config/database_pg.go b/config/database_pg.go index a7da471e..1acee98f 100644 --- a/config/database_pg.go +++ b/config/database_pg.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "nofx/crypto" "nofx/market" "os" "slices" @@ -16,7 +17,8 @@ import ( // PostgreSQLDatabase PostgreSQL数据库配置 type PostgreSQLDatabase struct { - db *sql.DB + db *sql.DB + cryptoService *crypto.CryptoService } // NewPostgreSQLDatabase 创建PostgreSQL数据库连接 @@ -60,6 +62,42 @@ func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) { return database, nil } +func (d *PostgreSQLDatabase) SetCryptoService(cs *crypto.CryptoService) { + d.cryptoService = cs +} + +func (d *PostgreSQLDatabase) encryptValue(value string, aadParts ...string) (string, error) { + if value == "" { + return "", nil + } + if d.cryptoService == nil { + return "", fmt.Errorf("crypto service not initialized") + } + if !d.cryptoService.HasDataKey() { + return "", fmt.Errorf("data encryption key not configured") + } + if d.cryptoService.IsEncryptedStorageValue(value) { + return value, nil + } + return d.cryptoService.EncryptForStorage(value, aadParts...) +} + +func (d *PostgreSQLDatabase) decryptValue(value string, aadParts ...string) (string, error) { + if value == "" { + return "", nil + } + if d.cryptoService == nil { + return "", fmt.Errorf("crypto service not initialized") + } + if !d.cryptoService.HasDataKey() { + return "", fmt.Errorf("data encryption key not configured") + } + if !d.cryptoService.IsEncryptedStorageValue(value) { + return "", fmt.Errorf("value is not encrypted") + } + return d.cryptoService.DecryptFromStorage(value, aadParts...) +} + // getEnv 获取环境变量,如果不存在返回默认值 func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { @@ -162,6 +200,15 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error if err != nil { return nil, err } + + if model.APIKey != "" { + decrypted, err := d.decryptValue(model.APIKey, model.UserID, model.ID, "api_key") + if err != nil { + return nil, err + } + model.APIKey = decrypted + } + models = append(models, &model) } @@ -216,7 +263,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s (通过provider匹配)", userID, existingID) return nil } - + // 没有找到配置,返回成功(幂等性) log.Printf("ℹ️ UpdateAIModel: 模型配置不存在,跳过删除: %s", id) return nil @@ -229,11 +276,18 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK `, userID, id).Scan(&existingID) if err == nil { + apiKeyEnc, err := d.encryptValue(apiKey, userID, existingID, "api_key") + if err != nil { + return err + } // 找到了现有配置(精确匹配 ID),更新它 _, err = d.db.Exec(` UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $5 AND user_id = $6 - `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) + `, enabled, apiKeyEnc, customAPIURL, customModelName, existingID, userID) + return err + } + if err != sql.ErrNoRows { return err } @@ -244,12 +298,19 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK `, userID, provider).Scan(&existingID) if err == nil { + apiKeyEnc, err := d.encryptValue(apiKey, userID, existingID, "api_key") + if err != nil { + return err + } // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) _, err = d.db.Exec(` UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $5 AND user_id = $6 - `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) + `, enabled, apiKeyEnc, customAPIURL, customModelName, existingID, userID) + return err + } + if err != sql.ErrNoRows { return err } @@ -292,11 +353,16 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK newModelID = fmt.Sprintf("%s_%s", userID, provider) } + apiKeyEnc, err := d.encryptValue(apiKey, userID, newModelID, "api_key") + if err != nil { + return err + } + log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) _, err = d.db.Exec(` INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - `, newModelID, userID, name, provider, enabled, apiKey, customAPIURL, customModelName) + `, newModelID, userID, name, provider, enabled, apiKeyEnc, customAPIURL, customModelName) return err } @@ -309,6 +375,7 @@ func (d *PostgreSQLDatabase) GetExchanges(userID string) ([]*ExchangeConfig, err COALESCE(aster_user, '') AS aster_user, COALESCE(aster_signer, '') AS aster_signer, COALESCE(aster_private_key, '') AS aster_private_key, + COALESCE(dex_wallet_private_key, '') AS dex_wallet_private_key, COALESCE(deleted, FALSE) AS deleted, created_at, updated_at FROM exchanges @@ -329,12 +396,50 @@ func (d *PostgreSQLDatabase) GetExchanges(userID string) ([]*ExchangeConfig, err &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, + &exchange.DEXWalletPrivateKey, &exchange.Deleted, &exchange.CreatedAt, &exchange.UpdatedAt, ) if err != nil { return nil, err } + + if decrypted, err := d.decryptValue(exchange.APIKey, exchange.UserID, exchange.ID, "api_key"); err == nil { + exchange.APIKey = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.SecretKey, exchange.UserID, exchange.ID, "secret_key"); err == nil { + exchange.SecretKey = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.HyperliquidWalletAddr, exchange.UserID, exchange.ID, "hyperliquid_wallet_addr"); err == nil { + exchange.HyperliquidWalletAddr = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.AsterUser, exchange.UserID, exchange.ID, "aster_user"); err == nil { + exchange.AsterUser = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.AsterSigner, exchange.UserID, exchange.ID, "aster_signer"); err == nil { + exchange.AsterSigner = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.AsterPrivateKey, exchange.UserID, exchange.ID, "aster_private_key"); err == nil { + exchange.AsterPrivateKey = decrypted + } else { + return nil, err + } + if decrypted, err := d.decryptValue(exchange.DEXWalletPrivateKey, exchange.UserID, exchange.ID, "dex_wallet_private_key"); err == nil { + exchange.DEXWalletPrivateKey = decrypted + } else { + return nil, err + } + exchanges = append(exchanges, &exchange) } @@ -345,7 +450,7 @@ func (d *PostgreSQLDatabase) GetExchanges(userID string) ([]*ExchangeConfig, err func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) - // 如果请求禁用该交易所,标记为已删除 + // 如果请求禁用该交易所,执行软删除 if !enabled { _, err := d.db.Exec(` UPDATE exchanges @@ -369,13 +474,38 @@ func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, api return nil } + apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") + if err != nil { + return fmt.Errorf("encrypt api_key failed: %w", err) + } + secretKeyEnc, err := d.encryptValue(secretKey, userID, id, "secret_key") + if err != nil { + return fmt.Errorf("encrypt secret_key failed: %w", err) + } + hyperAddrEnc, err := d.encryptValue(hyperliquidWalletAddr, userID, id, "hyperliquid_wallet_addr") + if err != nil { + return fmt.Errorf("encrypt hyperliquid_wallet_addr failed: %w", err) + } + asterUserEnc, err := d.encryptValue(asterUser, userID, id, "aster_user") + if err != nil { + return fmt.Errorf("encrypt aster_user failed: %w", err) + } + asterSignerEnc, err := d.encryptValue(asterSigner, userID, id, "aster_signer") + if err != nil { + return fmt.Errorf("encrypt aster_signer failed: %w", err) + } + asterPrivateKeyEnc, err := d.encryptValue(asterPrivateKey, userID, id, "aster_private_key") + if err != nil { + return fmt.Errorf("encrypt aster_private_key failed: %w", err) + } + // 首先尝试更新现有的用户配置 result, err := d.db.Exec(` UPDATE exchanges SET enabled = $1, api_key = $2, secret_key = $3, testnet = $4, hyperliquid_wallet_addr = $5, aster_user = $6, aster_signer = $7, aster_private_key = $8, deleted = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $9 AND user_id = $10 - `, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, id, userID) + `, enabled, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc, id, userID) if err != nil { log.Printf("❌ UpdateExchange: 更新失败: %v", err) return err @@ -418,7 +548,7 @@ func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, api hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, deleted, created_at, updated_at) VALUES ($1, $2, $3, $4, TRUE, $5, $6, $7, $8, $9, $10, $11, FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - `, id, userID, name, typ, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) + `, id, userID, name, typ, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc) if err != nil { log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) @@ -434,21 +564,51 @@ func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, api // CreateAIModel 创建AI模型配置 func (d *PostgreSQLDatabase) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error { - _, err := d.db.Exec(` + apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") + if err != nil { + return err + } + + _, err = d.db.Exec(` INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO NOTHING - `, id, userID, name, provider, enabled, apiKey, customAPIURL) + `, id, userID, name, provider, enabled, apiKeyEnc, customAPIURL) return err } // CreateExchange 创建交易所配置 func (d *PostgreSQLDatabase) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { - _, err := d.db.Exec(` + apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") + if err != nil { + return fmt.Errorf("encrypt api_key failed: %w", err) + } + secretKeyEnc, err := d.encryptValue(secretKey, userID, id, "secret_key") + if err != nil { + return fmt.Errorf("encrypt secret_key failed: %w", err) + } + hyperAddrEnc, err := d.encryptValue(hyperliquidWalletAddr, userID, id, "hyperliquid_wallet_addr") + if err != nil { + return fmt.Errorf("encrypt hyperliquid_wallet_addr failed: %w", err) + } + asterUserEnc, err := d.encryptValue(asterUser, userID, id, "aster_user") + if err != nil { + return fmt.Errorf("encrypt aster_user failed: %w", err) + } + asterSignerEnc, err := d.encryptValue(asterSigner, userID, id, "aster_signer") + if err != nil { + return fmt.Errorf("encrypt aster_signer failed: %w", err) + } + asterPrivateKeyEnc, err := d.encryptValue(asterPrivateKey, userID, id, "aster_private_key") + if err != nil { + return fmt.Errorf("encrypt aster_private_key failed: %w", err) + } + + _, err = d.db.Exec(` INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id, user_id) DO NOTHING - `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) + `, id, userID, name, typ, enabled, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc) return err } @@ -575,6 +735,57 @@ func (d *PostgreSQLDatabase) GetTraderConfig(userID, traderID string) (*TraderRe return nil, nil, nil, err } + if aiModel.APIKey != "" { + decrypted, err := d.decryptValue(aiModel.APIKey, aiModel.UserID, aiModel.ID, "api_key") + if err != nil { + return nil, nil, nil, err + } + aiModel.APIKey = decrypted + } + + if exchange.APIKey != "" { + decrypted, err := d.decryptValue(exchange.APIKey, exchange.UserID, exchange.ID, "api_key") + if err != nil { + return nil, nil, nil, err + } + exchange.APIKey = decrypted + } + if exchange.SecretKey != "" { + decrypted, err := d.decryptValue(exchange.SecretKey, exchange.UserID, exchange.ID, "secret_key") + if err != nil { + return nil, nil, nil, err + } + exchange.SecretKey = decrypted + } + if exchange.HyperliquidWalletAddr != "" { + decrypted, err := d.decryptValue(exchange.HyperliquidWalletAddr, exchange.UserID, exchange.ID, "hyperliquid_wallet_addr") + if err != nil { + return nil, nil, nil, err + } + exchange.HyperliquidWalletAddr = decrypted + } + if exchange.AsterUser != "" { + decrypted, err := d.decryptValue(exchange.AsterUser, exchange.UserID, exchange.ID, "aster_user") + if err != nil { + return nil, nil, nil, err + } + exchange.AsterUser = decrypted + } + if exchange.AsterSigner != "" { + decrypted, err := d.decryptValue(exchange.AsterSigner, exchange.UserID, exchange.ID, "aster_signer") + if err != nil { + return nil, nil, nil, err + } + exchange.AsterSigner = decrypted + } + if exchange.AsterPrivateKey != "" { + decrypted, err := d.decryptValue(exchange.AsterPrivateKey, exchange.UserID, exchange.ID, "aster_private_key") + if err != nil { + return nil, nil, nil, err + } + exchange.AsterPrivateKey = decrypted + } + return &trader, &aiModel, &exchange, nil } diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 00000000..9a29480f --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,394 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + storagePrefix = "ENC:v1:" + storageDelimiter = ":" + dataKeyEnvName = "DATA_ENCRYPTION_KEY" +) + +type EncryptedPayload struct { + WrappedKey string `json:"wrappedKey"` + IV string `json:"iv"` + Ciphertext string `json:"ciphertext"` + AAD string `json:"aad,omitempty"` + KID string `json:"kid,omitempty"` + TS int64 `json:"ts,omitempty"` +} + +type AADData struct { + UserID string `json:"userId"` + SessionID string `json:"sessionId"` + TS int64 `json:"ts"` + Purpose string `json:"purpose"` +} + +type CryptoService struct { + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey + dataKey []byte +} + +func NewCryptoService(privateKeyPath string) (*CryptoService, error) { + // 读取私钥文件 + privateKeyPEM, err := ioutil.ReadFile(privateKeyPath) + if err != nil { + // 如果私钥文件不存在,生成新的密钥对 + if err := GenerateRSAKeyPair(privateKeyPath); err != nil { + return nil, fmt.Errorf("failed to generate RSA key pair: %w", err) + } + privateKeyPEM, err = ioutil.ReadFile(privateKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to read generated private key: %w", err) + } + } + + // 解析私钥 + privateKey, err := ParseRSAPrivateKeyFromPEM(privateKeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + dataKey, err := loadDataKeyFromEnv() + if err != nil { + return nil, fmt.Errorf("failed to load data encryption key: %w", err) + } + + return &CryptoService{ + privateKey: privateKey, + publicKey: &privateKey.PublicKey, + dataKey: dataKey, + }, nil +} + +func GenerateRSAKeyPair(privateKeyPath string) error { + // 确保目录存在 + dir := filepath.Dir(privateKeyPath) + if dir != "." { + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + } + + // 生成 RSA 密钥对 + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + // 编码私钥 + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + // 保存私钥 + if err := ioutil.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil { + return err + } + + // 编码公钥 + publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + return err + } + + publicKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyDER, + }) + + // 保存公钥 + publicKeyPath := privateKeyPath + ".pub" + if err := ioutil.WriteFile(publicKeyPath, publicKeyPEM, 0644); err != nil { + return err + } + + return nil +} + +func ParseRSAPrivateKeyFromPEM(pemBytes []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("no PEM block found") + } + + switch block.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("not an RSA key") + } + return rsaKey, nil + default: + return nil, errors.New("unsupported key type: " + block.Type) + } +} + +func loadDataKeyFromEnv() ([]byte, error) { + keyStr := strings.TrimSpace(os.Getenv(dataKeyEnvName)) + if keyStr == "" { + return nil, fmt.Errorf("%s not set", dataKeyEnvName) + } + + if key, ok := decodePossibleKey(keyStr); ok { + return key, nil + } + + sum := sha256.Sum256([]byte(keyStr)) + key := make([]byte, len(sum)) + copy(key, sum[:]) + return key, nil +} + +func decodePossibleKey(value string) ([]byte, bool) { + decoders := []func(string) ([]byte, error){ + base64.StdEncoding.DecodeString, + base64.RawStdEncoding.DecodeString, + func(s string) ([]byte, error) { return hex.DecodeString(s) }, + } + + for _, decoder := range decoders { + if decoded, err := decoder(value); err == nil { + if key, ok := normalizeAESKey(decoded); ok { + return key, true + } + } + } + + return nil, false +} + +func normalizeAESKey(raw []byte) ([]byte, bool) { + switch len(raw) { + case 16, 24, 32: + return raw, true + case 0: + return nil, false + default: + sum := sha256.Sum256(raw) + key := make([]byte, len(sum)) + copy(key, sum[:]) + return key, true + } +} + +func (cs *CryptoService) HasDataKey() bool { + return len(cs.dataKey) > 0 +} + +func (cs *CryptoService) GetPublicKeyPEM() string { + publicKeyDER, err := x509.MarshalPKIXPublicKey(cs.publicKey) + if err != nil { + return "" + } + + publicKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyDER, + }) + + return string(publicKeyPEM) +} + +func (cs *CryptoService) EncryptForStorage(plaintext string, aadParts ...string) (string, error) { + if plaintext == "" { + return "", nil + } + if !cs.HasDataKey() { + return "", errors.New("data encryption key not configured") + } + if isEncryptedStorageValue(plaintext) { + return plaintext, nil + } + + block, err := aes.NewCipher(cs.dataKey) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", err + } + + aad := composeAAD(aadParts) + ciphertext := gcm.Seal(nil, nonce, []byte(plaintext), aad) + + return storagePrefix + + base64.StdEncoding.EncodeToString(nonce) + storageDelimiter + + base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func (cs *CryptoService) DecryptFromStorage(value string, aadParts ...string) (string, error) { + if value == "" { + return "", nil + } + if !cs.HasDataKey() { + return "", errors.New("data encryption key not configured") + } + if !isEncryptedStorageValue(value) { + return "", errors.New("value is not encrypted") + } + + payload := strings.TrimPrefix(value, storagePrefix) + parts := strings.SplitN(payload, storageDelimiter, 2) + if len(parts) != 2 { + return "", errors.New("invalid encrypted payload format") + } + + nonce, err := base64.StdEncoding.DecodeString(parts[0]) + if err != nil { + return "", fmt.Errorf("decode nonce failed: %w", err) + } + + ciphertext, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("decode ciphertext failed: %w", err) + } + + block, err := aes.NewCipher(cs.dataKey) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + if len(nonce) != gcm.NonceSize() { + return "", fmt.Errorf("invalid nonce size: expected %d, got %d", gcm.NonceSize(), len(nonce)) + } + + aad := composeAAD(aadParts) + plaintext, err := gcm.Open(nil, nonce, ciphertext, aad) + if err != nil { + return "", fmt.Errorf("decryption failed: %w", err) + } + + return string(plaintext), nil +} + +func (cs *CryptoService) IsEncryptedStorageValue(value string) bool { + return isEncryptedStorageValue(value) +} + +func composeAAD(parts []string) []byte { + if len(parts) == 0 { + return nil + } + return []byte(strings.Join(parts, "|")) +} + +func isEncryptedStorageValue(value string) bool { + return strings.HasPrefix(value, storagePrefix) +} + +func (cs *CryptoService) DecryptPayload(payload *EncryptedPayload) ([]byte, error) { + // 1. 验证时间戳(防止重放攻击) + if payload.TS != 0 { + elapsed := time.Since(time.Unix(payload.TS, 0)) + if elapsed > 5*time.Minute || elapsed < -1*time.Minute { + return nil, errors.New("timestamp invalid or expired") + } + } + + // 2. 解码 base64url + wrappedKey, err := base64.RawURLEncoding.DecodeString(payload.WrappedKey) + if err != nil { + return nil, fmt.Errorf("failed to decode wrapped key: %w", err) + } + + iv, err := base64.RawURLEncoding.DecodeString(payload.IV) + if err != nil { + return nil, fmt.Errorf("failed to decode IV: %w", err) + } + + ciphertext, err := base64.RawURLEncoding.DecodeString(payload.Ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decode ciphertext: %w", err) + } + + var aad []byte + if payload.AAD != "" { + aad, err = base64.RawURLEncoding.DecodeString(payload.AAD) + if err != nil { + return nil, fmt.Errorf("failed to decode AAD: %w", err) + } + + // 验证 AAD + var aadData AADData + if err := json.Unmarshal(aad, &aadData); err == nil { + // 可以在这里添加额外的验证逻辑 + // 例如:验证 sessionID、userID 等 + } + } + + // 3. 使用 RSA-OAEP 解密 AES 密钥 + aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, cs.privateKey, wrappedKey, nil) + if err != nil { + return nil, fmt.Errorf("failed to unwrap AES key: %w", err) + } + + // 4. 使用 AES-GCM 解密数据 + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + if len(iv) != gcm.NonceSize() { + return nil, fmt.Errorf("invalid IV size: expected %d, got %d", gcm.NonceSize(), len(iv)) + } + + // 解密并验证认证标签 + plaintext, err := gcm.Open(nil, iv, ciphertext, aad) + if err != nil { + return nil, fmt.Errorf("authentication/decryption failed: %w", err) + } + + return plaintext, nil +} + +func (cs *CryptoService) DecryptSensitiveData(payload *EncryptedPayload) (string, error) { + plaintext, err := cs.DecryptPayload(payload) + if err != nil { + return "", err + } + return string(plaintext), nil +} diff --git a/docker-compose.yml b/docker-compose.yml index acdf459a..a15a01de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,7 @@ services: environment: - TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone - AI_MAX_TOKENS=4000 # AI响应的最大token数(默认2000,建议4000-8000) + - DATA_ENCRYPTION_KEY=${DATA_ENCRYPTION_KEY} # 数据加密密钥 - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - POSTGRES_DB=${POSTGRES_DB:-nofx} diff --git a/main.go b/main.go index 73dbab1b..dee1082e 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "nofx/api" "nofx/auth" "nofx/config" + "nofx/crypto" "nofx/manager" "nofx/market" "nofx/pool" @@ -171,6 +172,13 @@ func main() { } defer database.Close() + // 初始化加密服务(用于敏感数据加密存储与传输) + cryptoService, err := crypto.NewCryptoService("keys/rsa_private.key") + if err != nil { + log.Fatalf("❌ 初始化加密服务失败: %v", err) + } + database.SetCryptoService(cryptoService) + // 同步config.json到数据库 if err := syncConfigToDatabase(database, configFile); err != nil { log.Printf("⚠️ 同步config.json到数据库失败: %v", err) @@ -289,7 +297,7 @@ func main() { } // 创建并启动API服务器 - apiServer := api.NewServer(traderManager, database, apiPort) + apiServer := api.NewServer(traderManager, database, cryptoService, apiPort) go func() { if err := apiServer.Start(); err != nil { log.Printf("❌ API服务器错误: %v", err) diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 198821bd..87833f6c 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -13,6 +13,7 @@ import { useAuth } from '../contexts/AuthContext' import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' +import { TwoStageKeyModal } from './TwoStageKeyModal' import { Bot, Brain, @@ -46,6 +47,12 @@ function getShortName(fullName: string): string { return parts.length > 1 ? parts[parts.length - 1] : fullName } +function maskSecret(value: string): string { + if (!value) return '' + const length = Math.min(value.length, 16) + return '•'.repeat(length) +} + interface AITradersPageProps { onTraderSelect?: (traderId: string) => void } @@ -445,7 +452,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }, } - await api.updateExchangeConfigs(request) + await api.updateExchangeConfigsEncrypted(request) const refreshed = await api.getExchangeConfigs() setAllExchanges(refreshed) @@ -494,7 +501,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }, } - await api.updateExchangeConfigs(request) + await api.updateExchangeConfigsEncrypted(request) const refreshedExchanges = await api.getExchangeConfigs() setAllExchanges(refreshedExchanges) @@ -1666,6 +1673,9 @@ function ExchangeConfigModal({ const [asterUser, setAsterUser] = useState('') const [asterSigner, setAsterSigner] = useState('') const [asterPrivateKey, setAsterPrivateKey] = useState('') + const [secureInputTarget, setSecureInputTarget] = useState< + null | 'hyperliquid' | 'aster' + >(null) // 获取当前选择的交易所信息 // 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等) @@ -1674,6 +1684,13 @@ function ExchangeConfigModal({ ? configuredExchanges?.find(e => e.id === selectedExchangeId) : supportedExchanges?.find(e => e.id === selectedExchangeId); + const secureInputContextLabel = + secureInputTarget === 'aster' + ? t('asterExchangeName', language) + : secureInputTarget === 'hyperliquid' + ? t('hyperliquidExchangeName', language) + : undefined + // 如果是编辑现有交易所,初始化表单数据 useEffect(() => { if (editingExchangeId && selectedExchange) { @@ -1692,6 +1709,28 @@ function ExchangeConfigModal({ } }, [editingExchangeId, selectedExchange]) + const handleSecureInputComplete = ({ + value, + obfuscationLog, + }: { + value: string + obfuscationLog: string[] + }) => { + const trimmed = value.trim() + if (secureInputTarget === 'hyperliquid') { + setApiKey(trimmed) + } + if (secureInputTarget === 'aster') { + setAsterPrivateKey(trimmed) + } + console.log('Secure input obfuscation log:', obfuscationLog) + setSecureInputTarget(null) + } + + const handleSecureInputCancel = () => { + setSecureInputTarget(null) + } + // 加载服务器IP(当选择binance时) useEffect(() => { if (selectedExchangeId === 'binance' && !serverIP) { @@ -1755,11 +1794,12 @@ function ExchangeConfigModal({ } return ( -
+ {t('twoStageModalDescription', language, { length: expectedLength })} +
++ {t('twoStageStage1Hint', language)} +
+
+ {manualObfuscationValue}
+
+ )}
+ + {t('twoStageStage2Hint', language)} +
+