package cmd import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "fmt" "os" "strings" "github.com/gin-gonic/gin" "gopkg.in/yaml.v3" ) // setupCryptoRoutes настраивает роуты для шифрования/дешифрования func setupCryptoRoutes(api *gin.RouterGroup, passHash [32]byte) { // Encrypt: вход YAML (и опционально path). Если path указан: можно не передавать yaml, // тогда читаем из файла и пишем шифровку в этот же путь api.POST("/encrypt", func(c *gin.Context) { var req struct { Yaml string `json:"yaml"` Path string `json:"path"` } if err := c.BindJSON(&req); err != nil { c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)}) return } if strings.TrimSpace(req.Yaml) == "" { c.JSON(400, gin.H{"error": "yaml is required"}) return } // Извлекаем path из YAML для автоматической записи файла var yamlPath string if yamlStr := strings.TrimSpace(req.Yaml); yamlStr != "" { var config map[string]interface{} if err := yaml.Unmarshal([]byte(yamlStr), &config); err == nil { if pathVal, ok := config["path"].(string); ok { yamlPath = strings.TrimSpace(pathVal) } } } // Используем path из запроса или из YAML targetPath := strings.TrimSpace(req.Path) if targetPath == "" && yamlPath != "" { targetPath = yamlPath } encrypted, err := encryptBytes([]byte(req.Yaml), passHash[:]) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } out := "ENCRYPTED:" + encrypted if targetPath != "" { if !isFileIOEnabled() { c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"}) return } if err := os.WriteFile(targetPath, []byte(out), 0600); err != nil { c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)}) return } c.JSON(200, gin.H{"status": "ok", "encrypted": out, "path": targetPath}) return } c.JSON(200, gin.H{"encrypted": out}) }) // Decrypt: вход ENCRYPTED:... (и опционально path). Если path указан: можно не передавать encrypted, // тогда читаем из файла и пишем расшифровку в этот же путь api.POST("/decrypt", func(c *gin.Context) { var req struct { Encrypted string `json:"encrypted"` Path string `json:"path"` } if err := c.BindJSON(&req); err != nil { c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)}) return } var enc string if strings.TrimSpace(req.Path) != "" && strings.TrimSpace(req.Encrypted) == "" { if !isFileIOEnabled() { c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"}) return } data, err := os.ReadFile(req.Path) if err != nil { c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)}) return } enc = string(data) } else { enc = req.Encrypted } if !strings.HasPrefix(enc, "ENCRYPTED:") { c.JSON(400, gin.H{"error": "input must start with ENCRYPTED:"}) return } plain, err := decryptString(enc[10:], passHash[:]) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Извлекаем path из расшифрованного YAML var yamlPath string if plainStr := string(plain); strings.TrimSpace(plainStr) != "" { var config map[string]interface{} if err := yaml.Unmarshal(plain, &config); err == nil { if pathVal, ok := config["path"].(string); ok { yamlPath = strings.TrimSpace(pathVal) } } } // Используем path из запроса или из YAML targetPath := strings.TrimSpace(req.Path) if targetPath == "" && yamlPath != "" { targetPath = yamlPath } if targetPath != "" { if !isFileIOEnabled() { c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"}) return } if err := os.WriteFile(targetPath, plain, 0600); err != nil { c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)}) return } c.JSON(200, gin.H{"status": "ok", "yaml": string(plain), "path": targetPath}) return } c.JSON(200, gin.H{"yaml": string(plain)}) }) // Encrypt file on server filesystem api.POST("/encrypt-file", func(c *gin.Context) { if !isFileIOEnabled() { c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"}) return } var req struct { Path string `json:"path"` } if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" { c.JSON(400, gin.H{"error": "invalid path"}) return } data, err := os.ReadFile(req.Path) if err != nil { c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)}) return } encrypted, err := encryptBytes(data, passHash[:]) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } out := "ENCRYPTED:" + encrypted if err := os.WriteFile(req.Path, []byte(out), 0600); err != nil { c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)}) return } c.JSON(200, gin.H{"status": "ok", "encrypted": out}) }) // Decrypt file on server filesystem api.POST("/decrypt-file", func(c *gin.Context) { if !isFileIOEnabled() { c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"}) return } var req struct { Path string `json:"path"` } if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" { c.JSON(400, gin.H{"error": "invalid path"}) return } data, err := os.ReadFile(req.Path) if err != nil { c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)}) return } enc := string(data) if !strings.HasPrefix(enc, "ENCRYPTED:") { c.JSON(400, gin.H{"error": "file must start with ENCRYPTED:"}) return } plain, err := decryptString(enc[10:], passHash[:]) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if err := os.WriteFile(req.Path, plain, 0600); err != nil { c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)}) return } c.JSON(200, gin.H{"status": "ok", "yaml": string(plain)}) }) } // encryptBytes шифрует байты с помощью AES-GCM func encryptBytes(data []byte, key []byte) (string, error) { block, err := aes.NewCipher(key) 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 } ciphertext := gcm.Seal(nonce, nonce, data, nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // decryptString дешифрует строку с помощью AES-GCM func decryptString(encrypted string, key []byte) ([]byte, error) { data, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonceSize := gcm.NonceSize() if len(data) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } return plaintext, nil }