Files
live-stego-experiments/stego-server/main.go
2025-05-22 17:22:27 +02:00

350 lines
7.9 KiB
Go

package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/auyer/steganography"
"golang.org/x/image/webp"
"gopkg.in/yaml.v3"
)
type Config struct {
Server struct {
Port string `yaml:"port"`
ImageDir string `yaml:"image_dir"`
BasePath string `yaml:"base_path"`
CacheImages bool `yaml:"cache_images"`
} `yaml:"server"`
Security struct {
AESKey string `yaml:"aes_key"` // Base64 encoded 32-byte key
} `yaml:"security"`
}
func loadConfig(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("config file read error: %w", err)
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("config parse error: %w", err)
}
// Set defaults if not specified
if cfg.Server.Port == "" {
cfg.Server.Port = "8219"
}
if cfg.Server.ImageDir == "" {
cfg.Server.ImageDir = "./images"
}
if cfg.Server.BasePath == "" {
cfg.Server.BasePath = "/"
}
return nil
}
var (
decodedImageCache sync.Map // path -> *cachedImage
aesKey []byte
cfg Config
)
type cachedImage struct {
img *image.NRGBA
mimeType string
maxPayloadSize uint32
size uint32
}
func init() {
if err := loadConfig("config.yaml"); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
if cfg.Security.AESKey != "" {
var err error
aesKey, err = base64.StdEncoding.DecodeString(cfg.Security.AESKey)
if err != nil {
log.Fatalf("Invalid AES key in config: %v", err)
}
} else {
aesKey = make([]byte, 32)
if _, err := rand.Read(aesKey); err != nil {
log.Fatalf("Failed to generate AES key: %v", err)
}
log.Printf("Generated new AES key (base64): %s", base64.StdEncoding.EncodeToString(aesKey))
}
if cfg.Server.CacheImages {
if err := loadImages(); err != nil {
log.Printf("Warning: could not preload images: %v", err)
}
}
}
func loadImages() error {
return filepath.WalkDir(cfg.Server.ImageDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".jpg", ".jpeg", ".png", ".webp":
data, err := os.ReadFile(path)
size := uint32(len(data))
if err != nil {
return err
}
// Preload decoded image
img, mimeType, err := decodeImageData(data, ext)
if err != nil {
return fmt.Errorf("error decoding %s: %v", path, err)
}
nrgba := image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
maxSize := steganography.MaxEncodeSize(nrgba)
maxPlaintext, err := maxPlaintextSize(maxSize)
if err != nil {
log.Printf("Skipping %s: %v", path, err)
return nil
}
decodedImageCache.Store(path, &cachedImage{
img: nrgba,
mimeType: mimeType,
maxPayloadSize: maxPlaintext,
size: size,
})
log.Printf("Cached decoded image: %s", path)
}
return nil
})
}
func decodeImageData(data []byte, ext string) (image.Image, string, error) {
switch ext {
case ".jpg", ".jpeg":
img, err := jpeg.Decode(bytes.NewReader(data))
return img, "image/jpeg", err
case ".png":
img, err := png.Decode(bytes.NewReader(data))
return img, "image/png", err
case ".webp":
img, err := webp.Decode(bytes.NewReader(data))
return img, "image/webp", err
default:
return nil, "", fmt.Errorf("unsupported format: %s", ext)
}
}
func encryptData(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func maxPlaintextSize(totalOutputSize uint32) (uint32, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return 0, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return 0, err
}
overhead := uint32(gcm.NonceSize() + gcm.Overhead())
if totalOutputSize < overhead {
return 0, fmt.Errorf("output size too small")
}
return totalOutputSize - overhead, nil
}
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func embedData(img *image.NRGBA, data []byte) ([]byte, error) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
if buf.Cap() <= 2<<20 { // 2MB cap
bufferPool.Put(buf)
}
}()
err := steganography.Encode(buf, img, data)
if err != nil {
return nil, fmt.Errorf("failed to encode data into image: %w", err)
}
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result, nil
}
func getClientInfo(r *http.Request) string {
ip := getClientIP(r)
var headers strings.Builder
for name, values := range r.Header {
for _, value := range values {
headers.WriteString(fmt.Sprintf("%s: %s\n", name, value))
}
}
return fmt.Sprintf("%s\n%s\n%s", time.Now().UTC().Format(time.RFC3339), ip, headers.String())
}
func getClientIP(r *http.Request) string {
headers := []string{"X-Forwarded-For"}
for _, h := range headers {
if ip := r.Header.Get(h); ip != "" {
parts := strings.Split(ip, ",")
clientIP := strings.TrimSpace(parts[0])
if len(parts) > 1 && isTrustedProxy(clientIP) {
return strings.TrimSpace(parts[1])
}
return clientIP
}
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}
func isTrustedProxy(ip string) bool {
return true // Implement actual IP validation if needed
}
func imageHandler(w http.ResponseWriter, r *http.Request) {
relativePath := strings.TrimPrefix(r.URL.Path, cfg.Server.BasePath)
requestedPath := filepath.Clean(strings.TrimPrefix(relativePath, "/"))
imagePath := filepath.Join(cfg.Server.ImageDir, requestedPath)
log.Printf("Requested image path: %s", imagePath)
if !strings.HasPrefix(imagePath, filepath.Clean(cfg.Server.ImageDir)+string(os.PathSeparator)) {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
if _, err := os.Stat(imagePath); errors.Is(err, os.ErrNotExist) {
http.NotFound(w, r)
return
}
var ci *cachedImage
if cached, ok := decodedImageCache.Load(imagePath); ok {
ci = cached.(*cachedImage)
} else {
data, err := os.ReadFile(imagePath)
if err != nil {
http.Error(w, "File read error", http.StatusInternalServerError)
return
}
ext := strings.ToLower(filepath.Ext(imagePath))
img, mimeType, err := decodeImageData(data, ext)
if err != nil {
http.ServeFile(w, r, imagePath)
return
}
nrgba := image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
maxSize := steganography.MaxEncodeSize(nrgba)
maxPlaintext, err := maxPlaintextSize(maxSize)
if err != nil {
http.ServeFile(w, r, imagePath)
return
}
ci = &cachedImage{
img: nrgba,
mimeType: mimeType,
maxPayloadSize: maxPlaintext,
size: uint32(len(data)),
}
decodedImageCache.Store(imagePath, ci)
}
metadata := getClientInfo(r)
if len(metadata) > int(ci.maxPayloadSize) {
metadata = metadata[:ci.maxPayloadSize]
}
encryptedData, err := encryptData([]byte(metadata))
if err != nil {
http.ServeFile(w, r, imagePath)
return
}
modifiedImage, err := embedData(ci.img, encryptedData)
if err != nil {
http.ServeFile(w, r, imagePath)
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", fmt.Sprint(len(modifiedImage)))
w.Write(modifiedImage)
}
func main() {
if _, err := os.Stat(cfg.Server.ImageDir); os.IsNotExist(err) {
log.Fatalf("Image directory does not exist: %s", cfg.Server.ImageDir)
}
basePath := strings.TrimSuffix(cfg.Server.BasePath, "/")
http.HandleFunc(basePath+"/", imageHandler)
log.Printf("Starting server on :%s", cfg.Server.Port)
log.Fatal(http.ListenAndServe(":"+cfg.Server.Port, nil))
}