This commit is contained in:
66 changed files with 19815 additions and 0 deletions

258
back/cmd/crypto_routes.go Normal file
View File

@@ -0,0 +1,258 @@
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
}