package node import ( "archive/tar" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "os" "path/filepath" "forge.lthn.ai/Snider/Borg/pkg/datanode" "forge.lthn.ai/Snider/Borg/pkg/tim" ) // bundle.Type = BundleProfile // config/profile JSON only // bundle.Type = BundleMiner // miner binary + config // bundle.Type = BundleFull // miner binary + profiles + config type BundleType string const ( BundleProfile BundleType = "profile" // Just config/profile JSON BundleMiner BundleType = "miner" // Miner binary + config BundleFull BundleType = "full" // Everything (miner + profiles + config) ) // bundle, err := CreateProfileBundle(profileJSON, "main", password) // bundle, err := CreateMinerBundle("/usr/bin/xmrig", nil, "xmrig-lthn", password) // if VerifyBundle(bundle) { conn.Send(bundle) } type Bundle struct { Type BundleType `json:"type"` Name string `json:"name"` Data []byte `json:"data"` // Encrypted STIM data or raw JSON Checksum string `json:"checksum"` // SHA-256 of Data } // manifest := BundleManifest{Type: BundleMiner, Name: "xmrig-lthn", MinerType: "xmrig", CreatedAt: "2026-01-01T00:00:00Z"} type BundleManifest struct { Type BundleType `json:"type"` Name string `json:"name"` Version string `json:"version,omitempty"` MinerType string `json:"minerType,omitempty"` ProfileIDs []string `json:"profileIds,omitempty"` CreatedAt string `json:"createdAt"` } // bundle, err := CreateProfileBundle(profileJSON, "main", password) // if err != nil { return nil, err } func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config timContainer, err := tim.New() if err != nil { return nil, fmt.Errorf("failed to create TIM: %w", err) } timContainer.Config = profileJSON // Encrypt to STIM format stimData, err := timContainer.ToSigil(password) if err != nil { return nil, fmt.Errorf("failed to encrypt bundle: %w", err) } // Calculate checksum checksum := calculateChecksum(stimData) return &Bundle{ Type: BundleProfile, Name: name, Data: stimData, Checksum: checksum, }, nil } // bundle, err := CreateProfileBundleUnencrypted(profileJSON, "main") // if err != nil { return nil, err } // no password needed for trusted networks func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error) { checksum := calculateChecksum(profileJSON) return &Bundle{ Type: BundleProfile, Name: name, Data: profileJSON, Checksum: checksum, }, nil } // bundle, err := CreateMinerBundle("/usr/bin/xmrig", profileJSON, "xmrig-lthn", password) // if err != nil { return nil, err } func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary minerData, err := os.ReadFile(minerPath) if err != nil { return nil, fmt.Errorf("failed to read miner binary: %w", err) } // Create a tarball with the miner binary tarData, err := createTarball(map[string][]byte{ filepath.Base(minerPath): minerData, }) if err != nil { return nil, fmt.Errorf("failed to create tarball: %w", err) } // Create DataNode from tarball dataNode, err := datanode.FromTar(tarData) if err != nil { return nil, fmt.Errorf("failed to create datanode: %w", err) } // Create TIM from DataNode timContainer, err := tim.FromDataNode(dataNode) if err != nil { return nil, fmt.Errorf("failed to create TIM: %w", err) } // Set profile as config if provided if profileJSON != nil { timContainer.Config = profileJSON } // Encrypt to STIM format stimData, err := timContainer.ToSigil(password) if err != nil { return nil, fmt.Errorf("failed to encrypt bundle: %w", err) } checksum := calculateChecksum(stimData) return &Bundle{ Type: BundleMiner, Name: name, Data: stimData, Checksum: checksum, }, nil } // profileJSON, err := ExtractProfileBundle(bundle, password) // if err != nil { return nil, err } func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { return nil, fmt.Errorf("checksum mismatch - bundle may be corrupted") } // If it's unencrypted JSON, just return it if isJSON(bundle.Data) { return bundle.Data, nil } // Decrypt STIM format timContainer, err := tim.FromSigil(bundle.Data, password) if err != nil { return nil, fmt.Errorf("failed to decrypt bundle: %w", err) } return timContainer.Config, nil } // minerPath, profileJSON, err := ExtractMinerBundle(bundle, password, "/tmp/miners/xmrig") // if err != nil { return nil, err } func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { return "", nil, fmt.Errorf("checksum mismatch - bundle may be corrupted") } // Decrypt STIM format timContainer, err := tim.FromSigil(bundle.Data, password) if err != nil { return "", nil, fmt.Errorf("failed to decrypt bundle: %w", err) } // Convert rootfs to tarball and extract tarData, err := timContainer.RootFS.ToTar() if err != nil { return "", nil, fmt.Errorf("failed to convert rootfs to tar: %w", err) } // Extract tarball to destination minerPath, err := extractTarball(tarData, destDir) if err != nil { return "", nil, fmt.Errorf("failed to extract tarball: %w", err) } return minerPath, timContainer.Config, nil } // if !node.VerifyBundle(bundle) { return fmt.Errorf("bundle corrupted or tampered") } func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } // checksum := calculateChecksum(stimData) // bundle.Checksum = checksum func calculateChecksum(data []byte) string { hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } // if isJSON(bundle.Data) { return bundle.Data, nil } // skip decrypt for plain profiles // if isJSON(content) { mode = 0644 } else { mode = 0755 } // executable unless JSON func isJSON(data []byte) bool { if len(data) == 0 { return false } // JSON typically starts with { or [ return data[0] == '{' || data[0] == '[' } // tarData, err := createTarball(map[string][]byte{"xmrig": binaryData, "config.json": cfgData}) // if err != nil { return nil, err } func createTarball(files map[string][]byte) ([]byte, error) { var buffer bytes.Buffer tarWriter := tar.NewWriter(&buffer) // Track directories we've created directories := make(map[string]bool) for name, content := range files { // Create parent directories if needed directory := filepath.Dir(name) if directory != "." && !directories[directory] { header := &tar.Header{ Name: directory + "/", Mode: 0755, Typeflag: tar.TypeDir, } if err := tarWriter.WriteHeader(header); err != nil { return nil, err } directories[directory] = true } // Determine file mode (executable for binaries in miners/) fileMode := int64(0644) if filepath.Dir(name) == "miners" || !isJSON(content) { fileMode = 0755 } header := &tar.Header{ Name: name, Mode: fileMode, Size: int64(len(content)), } if err := tarWriter.WriteHeader(header); err != nil { return nil, err } if _, err := tarWriter.Write(content); err != nil { return nil, err } } if err := tarWriter.Close(); err != nil { return nil, err } return buffer.Bytes(), nil } // minerPath, err := extractTarball(tarData, "/tmp/miners/xmrig") // if err != nil { return "", nil, err } func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir, err := filepath.Abs(destDir) if err != nil { return "", fmt.Errorf("failed to resolve destination directory: %w", err) } absDestDir = filepath.Clean(absDestDir) if err := os.MkdirAll(absDestDir, 0755); err != nil { return "", err } tarReader := tar.NewReader(bytes.NewReader(tarData)) var firstExecutable string for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return "", err } // Security: Sanitize the tar entry name to prevent path traversal (Zip Slip) cleanName := filepath.Clean(header.Name) // Reject absolute paths if filepath.IsAbs(cleanName) { return "", fmt.Errorf("invalid tar entry: absolute path not allowed: %s", header.Name) } // Reject paths that escape the destination directory parentPrefix := ".." + string(os.PathSeparator) if bytes.HasPrefix([]byte(cleanName), []byte(parentPrefix)) || cleanName == ".." { return "", fmt.Errorf("invalid tar entry: path traversal attempt: %s", header.Name) } // Build the full path and verify it's within destDir fullPath := filepath.Join(absDestDir, cleanName) fullPath = filepath.Clean(fullPath) // Final security check: ensure the path is still within destDir destDirPrefix := absDestDir + string(os.PathSeparator) if !bytes.HasPrefix([]byte(fullPath), []byte(destDirPrefix)) && fullPath != absDestDir { return "", fmt.Errorf("invalid tar entry: path escape attempt: %s", header.Name) } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(fullPath, os.FileMode(header.Mode)); err != nil { return "", err } case tar.TypeReg: // Ensure parent directory exists if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { return "", err } outputFile, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { return "", err } // Limit file size to prevent decompression bombs (100MB max per file) const maxFileSize int64 = 100 * 1024 * 1024 limitedReader := io.LimitReader(tarReader, maxFileSize+1) written, err := io.Copy(outputFile, limitedReader) outputFile.Close() if err != nil { return "", err } if written > maxFileSize { os.Remove(fullPath) return "", fmt.Errorf("file %s exceeds maximum size of %d bytes", header.Name, maxFileSize) } // Track first executable if header.Mode&0111 != 0 && firstExecutable == "" { firstExecutable = fullPath } // Explicitly ignore symlinks and hard links to prevent symlink attacks case tar.TypeSymlink, tar.TypeLink: // Skip symlinks and hard links for security continue } } return firstExecutable, nil } // StreamBundle(bundle, conn) // stream to peer over WebSocket or HTTP body // StreamBundle(bundle, os.Stdout) // debug: print bundle JSON to stdout func StreamBundle(bundle *Bundle, w io.Writer) error { encoder := json.NewEncoder(w) return encoder.Encode(bundle) } // bundle, err := ReadBundle(conn) // read from WebSocket or HTTP response body // if err != nil { return nil, fmt.Errorf("read bundle: %w", err) } func ReadBundle(r io.Reader) (*Bundle, error) { var bundle Bundle decoder := json.NewDecoder(r) if err := decoder.Decode(&bundle); err != nil { return nil, err } return &bundle, nil }