Files
knock-gui/back/cmd/crypto_routes.go
2025-08-17 00:43:58 +06:00

259 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}