259 lines
7.5 KiB
Go
259 lines
7.5 KiB
Go
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
|
||
}
|