diff --git a/README.md b/README.md index 70305f7..7940cc5 100644 --- a/README.md +++ b/README.md @@ -279,19 +279,20 @@ Thanks to all our amazing contributors! - - ABHINAVGARG05 + + Janmesh23
- Abhinav Garg + Janmesh
- - Janmesh23 + + ABHINAVGARG05
- Janmesh + Abhinav Garg
+ Akash29g diff --git a/cmd/discover.go b/cmd/discover.go index 1c40111..b998165 100644 --- a/cmd/discover.go +++ b/cmd/discover.go @@ -9,13 +9,16 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" "github.com/spf13/cobra" "github.com/substantialcattle5/sietch/internal/config" "github.com/substantialcattle5/sietch/internal/discover" "github.com/substantialcattle5/sietch/internal/p2p" + "github.com/substantialcattle5/sietch/internal/ui" ) // discoverCmd represents the discover command @@ -28,15 +31,20 @@ This command creates a temporary libp2p node that broadcasts its presence and listens for other Sietch vaults on the local network. When peers are discovered, their information is displayed, including their peer ID and addresses. -Example: +With the --select flag, you can interactively choose which peers to pair with +for selective key exchange, providing fine-grained control over trust relationships. + +Examples: sietch discover # Run discovery with default settings sietch discover --timeout 30 # Run discovery for 30 seconds sietch discover --continuous # Run discovery until interrupted + sietch discover --select # Interactive peer selection for pairing sietch discover --port 9001 # Use a specific port for the libp2p node`, RunE: func(cmd *cobra.Command, args []string) error { // Get command flags timeout, _ := cmd.Flags().GetInt("timeout") continuous, _ := cmd.Flags().GetBool("continuous") + selectMode, _ := cmd.Flags().GetBool("select") port, _ := cmd.Flags().GetInt("port") verbose, _ := cmd.Flags().GetBool("verbose") vaultPath, _ := cmd.Flags().GetString("vault-path") @@ -101,6 +109,9 @@ Example: defer func() { _ = discovery.Stop() }() // Run the discovery loop + if selectMode { + return runDiscoveryWithSelection(ctx, host, syncService, peerChan, timeout, continuous) + } return discover.RunDiscoveryLoop(ctx, host, syncService, peerChan, timeout, continuous) }, } @@ -119,7 +130,134 @@ func init() { // Add command flags discoverCmd.Flags().IntP("timeout", "t", 60, "Discovery timeout in seconds (ignored with --continuous)") discoverCmd.Flags().BoolP("continuous", "c", false, "Run discovery continuously until interrupted") + discoverCmd.Flags().BoolP("select", "s", false, "Interactive peer selection mode for pairing") discoverCmd.Flags().IntP("port", "p", 0, "Port to use for libp2p (0 for random port)") discoverCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") discoverCmd.Flags().StringP("vault-path", "V", "", "Path to the vault directory (defaults to current directory)") } + +// runDiscoveryWithSelection runs discovery and allows interactive peer selection for pairing +func runDiscoveryWithSelection(ctx context.Context, host host.Host, syncService *p2p.SyncService, peerChan <-chan peer.AddrInfo, timeout int, continuous bool) error { + // Disable auto-trust for selective pairing + syncService.SetAutoTrustAllPeers(false) + + // Collect discovered peers + discoveredPeers := make([]peer.AddrInfo, 0) + discoveredPeersMap := make(map[peer.ID]bool) + + var timeoutChan <-chan time.Time + if !continuous { + timeoutChan = time.After(time.Duration(timeout) * time.Second) + fmt.Printf(" Discovery will run for %d seconds. Press Ctrl+C to stop earlier.\n\n", timeout) + } else { + fmt.Println(" Discovery will run until interrupted. Press Ctrl+C to stop.") + fmt.Println() + } + + fmt.Println("šŸ” Discovering peers for selection...") + + // Discovery loop + discoveryComplete := false + for !discoveryComplete { + select { + case p, ok := <-peerChan: + if !ok { + discoveryComplete = true + continue + } + + if p.ID == host.ID() || discoveredPeersMap[p.ID] { + continue + } + + discoveredPeersMap[p.ID] = true + discoveredPeers = append(discoveredPeers, p) + + fmt.Printf("āœ… Discovered peer #%d\n", len(discoveredPeers)) + fmt.Printf(" ID: %s\n", p.ID.String()) + fmt.Println(" Addresses:") + for _, addr := range p.Addrs { + fmt.Printf(" - %s\n", addr.String()) + } + fmt.Println() + + case <-timeoutChan: + fmt.Printf("\nāŒ› Discovery timeout reached after %d seconds.\n", timeout) + discoveryComplete = true + + case <-ctx.Done(): + fmt.Println("\nDiscovery interrupted") + discoveryComplete = true + } + } + + if len(discoveredPeers) == 0 { + fmt.Println("No peers discovered. Make sure other Sietch vaults are running and discoverable.") + return nil + } + + // Let user select peers + selectedPeers, err := ui.SelectPeersInteractively(discoveredPeers) + if err != nil { + return fmt.Errorf("peer selection failed: %v", err) + } + + if len(selectedPeers) == 0 { + fmt.Println("No peers selected for pairing.") + return nil + } + + // Set up pairing window (5 minutes default) + windowDuration := 5 * time.Minute + until := time.Now().Add(windowDuration) + + // Request pairing with selected peers + for _, peerID := range selectedPeers { + syncService.RequestPair(peerID, until) + } + + fmt.Printf("\nā° Pairing window active for %v\n", windowDuration) + fmt.Println("Waiting for mutual pairing...") + + // Wait for pairing to complete + timeoutChan = time.After(windowDuration) + pairedCount := 0 + + for { + select { + case <-timeoutChan: + fmt.Printf("\nāŒ› Pairing window expired after %v\n", windowDuration) + fmt.Printf("Successfully paired with %d peer(s)\n", pairedCount) + return nil + + case <-ctx.Done(): + fmt.Printf("\nPairing interrupted. Successfully paired with %d peer(s)\n", pairedCount) + return nil + + default: + // Check if any selected peers have been successfully paired + for _, peerID := range selectedPeers { + if syncService.HasPeer(peerID) { + pairedCount++ + fmt.Printf("āœ… Successfully paired with peer: %s\n", peerID.String()) + + // Remove from selected list to avoid counting twice + for i, p := range selectedPeers { + if p == peerID { + selectedPeers = append(selectedPeers[:i], selectedPeers[i+1:]...) + break + } + } + } + } + + // If all peers are paired, we're done + if len(selectedPeers) == 0 { + fmt.Printf("\nšŸŽ‰ All selected peers successfully paired!\n") + return nil + } + + time.Sleep(1 * time.Second) + } + } +} diff --git a/cmd/pair.go b/cmd/pair.go new file mode 100644 index 0000000..d7c6602 --- /dev/null +++ b/cmd/pair.go @@ -0,0 +1,413 @@ +/* +Copyright Ā© 2025 SubstantialCattle5 +*/ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/spf13/cobra" + + "github.com/substantialcattle5/sietch/internal/config" + "github.com/substantialcattle5/sietch/internal/discover" + "github.com/substantialcattle5/sietch/internal/p2p" + "github.com/substantialcattle5/sietch/internal/ui" +) + +// pairCmd represents the pair command +var pairCmd = &cobra.Command{ + Use: "pair", + Short: "Pair with specific peers for selective key exchange", + Long: `Establish selective trust relationships with other Sietch vaults. + +This command allows you to choose which peers you want to exchange keys with, +providing fine-grained control over your vault's trust relationships. Key exchange +only occurs when both parties have mutually selected each other. + +Examples: + sietch pair --select # Interactive peer selection + sietch pair --allow-from # Allow specific peer to pair with you + sietch pair --allow-all --window 10m # Allow all peers for 10 minutes + sietch pair --select --window 5m # Select peers with 5-minute window`, + RunE: func(cmd *cobra.Command, args []string) error { + // Get command flags + selectMode, _ := cmd.Flags().GetBool("select") + allowFrom, _ := cmd.Flags().GetString("allow-from") + allowAll, _ := cmd.Flags().GetBool("allow-all") + window, _ := cmd.Flags().GetString("window") + port, _ := cmd.Flags().GetInt("port") + verbose, _ := cmd.Flags().GetBool("verbose") + vaultPath, _ := cmd.Flags().GetString("vault-path") + + // If no vault path specified, use current directory + if vaultPath == "" { + var err error + vaultPath, err = os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %v", err) + } + } + + // Parse window duration + windowDuration, err := parseWindowDuration(window) + if err != nil { + return fmt.Errorf("invalid window duration: %v", err) + } + + // Validate flags + if !selectMode && allowFrom == "" && !allowAll { + return fmt.Errorf("must specify one of: --select, --allow-from, or --allow-all") + } + + // Create a context with cancellation + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle interrupts gracefully + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signalChan + fmt.Println("\nReceived interrupt signal, shutting down...") + cancel() + }() + + // Create a libp2p host + host, err := p2p.CreateLibp2pHost(port) + if err != nil { + return fmt.Errorf("failed to create libp2p host: %v", err) + } + defer host.Close() + + fmt.Printf("šŸ”— Starting pairing mode with node ID: %s\n", host.ID().String()) + if verbose { + displayHostAddresses(host) + } + + // Create a vault manager + vaultMgr, err := config.NewManager(vaultPath) + if err != nil { + return fmt.Errorf("failed to create vault manager: %v", err) + } + + // Get vault config + vaultConfig, err := vaultMgr.GetConfig() + if err != nil { + return fmt.Errorf("failed to load vault configuration: %v", err) + } + + // Create sync service (with or without RSA) + syncService, err := discover.CreateSyncService(host, vaultMgr, vaultConfig, vaultPath, verbose) + if err != nil { + return fmt.Errorf("failed to create sync service: %v", err) + } + + // Disable auto-trust for selective pairing + syncService.SetAutoTrustAllPeers(false) + + // Set pairing window + syncService.SetPairingWindow(windowDuration) + + // Setup discovery + discovery, peerChan, err := discover.SetupDiscovery(ctx, host) + if err != nil { + return err + } + defer func() { _ = discovery.Stop() }() + + fmt.Printf("šŸ“” Discovering peers for %v...\n", windowDuration) + + // Collect discovered peers + discoveredPeers := make([]peer.AddrInfo, 0) + timeout := time.After(windowDuration) + + discoveryLoop: + for { + select { + case p, ok := <-peerChan: + if !ok { + break discoveryLoop + } + + if p.ID == host.ID() { + continue + } + + // Check if we already have this peer + alreadyFound := false + for _, existing := range discoveredPeers { + if existing.ID == p.ID { + alreadyFound = true + break + } + } + + if !alreadyFound { + discoveredPeers = append(discoveredPeers, p) + fmt.Printf("āœ… Discovered peer: %s\n", p.ID.String()) + if verbose { + for _, addr := range p.Addrs { + fmt.Printf(" └─ %s\n", addr.String()) + } + } + } + + case <-timeout: + fmt.Printf("\nāŒ› Discovery timeout reached after %v\n", windowDuration) + break discoveryLoop + + case <-ctx.Done(): + fmt.Println("\nDiscovery interrupted") + break discoveryLoop + } + } + + if len(discoveredPeers) == 0 { + fmt.Println("No peers discovered. Make sure other Sietch vaults are running and discoverable.") + return nil + } + + // Handle different pairing modes + if selectMode { + return handleSelectMode(ctx, host, syncService, discoveredPeers, windowDuration) + } else if allowFrom != "" { + return handleAllowFromMode(ctx, host, syncService, discoveredPeers, allowFrom, windowDuration) + } else if allowAll { + return handleAllowAllMode(ctx, host, syncService, discoveredPeers, windowDuration) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(pairCmd) + + // Add flags + pairCmd.Flags().Bool("select", false, "Interactive peer selection mode") + pairCmd.Flags().String("allow-from", "", "Comma-separated list of peer IDs to allow for incoming pairing") + pairCmd.Flags().Bool("allow-all", false, "Allow all discovered peers for incoming pairing") + pairCmd.Flags().String("window", "5m", "Pairing window duration (e.g., 5m, 10m, 1h)") + pairCmd.Flags().Int("port", 0, "Port for libp2p host (0 for random)") + pairCmd.Flags().Bool("verbose", false, "Enable verbose output") + pairCmd.Flags().String("vault-path", "", "Path to vault directory") +} + +// parseWindowDuration parses a duration string like "5m", "10m", "1h" +func parseWindowDuration(window string) (time.Duration, error) { + if window == "" { + return 5 * time.Minute, nil + } + + // Handle common formats + switch window { + case "1m", "1min": + return 1 * time.Minute, nil + case "5m", "5min": + return 5 * time.Minute, nil + case "10m", "10min": + return 10 * time.Minute, nil + case "30m", "30min": + return 30 * time.Minute, nil + case "1h", "1hour": + return 1 * time.Hour, nil + } + + // Try to parse as Go duration + duration, err := time.ParseDuration(window) + if err != nil { + return 0, fmt.Errorf("invalid duration format '%s': %w", window, err) + } + + // Limit to reasonable range + if duration < 1*time.Minute || duration > 1*time.Hour { + return 0, fmt.Errorf("duration must be between 1 minute and 1 hour") + } + + return duration, nil +} + +// handleSelectMode handles interactive peer selection +func handleSelectMode(ctx context.Context, host host.Host, syncService *p2p.SyncService, peers []peer.AddrInfo, windowDuration time.Duration) error { + fmt.Println("\nšŸŽÆ Interactive peer selection mode") + + // Let user select peers + selectedPeers, err := ui.SelectPeersInteractively(peers) + if err != nil { + return fmt.Errorf("peer selection failed: %v", err) + } + + if len(selectedPeers) == 0 { + fmt.Println("No peers selected for pairing.") + return nil + } + + // Set up pairing window + until := time.Now().Add(windowDuration) + + // Request pairing with selected peers + for _, peerID := range selectedPeers { + syncService.RequestPair(peerID, until) + } + + fmt.Printf("\nā° Pairing window active for %v\n", windowDuration) + fmt.Println("Waiting for mutual pairing...") + + // Wait for pairing to complete + return waitForPairing(ctx, host, syncService, selectedPeers, windowDuration) +} + +// handleAllowFromMode handles specific peer allowlist +func handleAllowFromMode(ctx context.Context, host host.Host, syncService *p2p.SyncService, peers []peer.AddrInfo, allowFrom string, windowDuration time.Duration) error { + fmt.Println("\nšŸ” Allowlist mode") + + // Parse peer IDs + peerIDStrings := strings.Split(allowFrom, ",") + allowedPeers := make([]peer.ID, 0, len(peerIDStrings)) + + for _, peerIDStr := range peerIDStrings { + peerIDStr = strings.TrimSpace(peerIDStr) + peerID, err := peer.Decode(peerIDStr) + if err != nil { + return fmt.Errorf("invalid peer ID '%s': %v", peerIDStr, err) + } + allowedPeers = append(allowedPeers, peerID) + } + + // Set up pairing window + until := time.Now().Add(windowDuration) + + // Allow incoming pairing from specified peers + for _, peerID := range allowedPeers { + syncService.AllowIncomingPair(peerID, until) + } + + fmt.Printf("āœ… Allowed incoming pairing from %d peer(s) for %v\n", len(allowedPeers), windowDuration) + + // Display our node ID for others to select + fmt.Printf("\nšŸ“¢ Your node ID: %s\n", host.ID().String()) + fmt.Println("Share this ID with others so they can select you for pairing.") + fmt.Println("Waiting for incoming pairing requests...") + + // Wait for pairing to complete + return waitForIncomingPairing(ctx, host, syncService, allowedPeers, windowDuration) +} + +// handleAllowAllMode handles allowing all discovered peers +func handleAllowAllMode(ctx context.Context, host host.Host, syncService *p2p.SyncService, peers []peer.AddrInfo, windowDuration time.Duration) error { + fmt.Println("\n🌐 Allow all mode") + + // Set up pairing window + until := time.Now().Add(windowDuration) + + // Allow incoming pairing from all discovered peers + for _, peer := range peers { + syncService.AllowIncomingPair(peer.ID, until) + } + + fmt.Printf("āœ… Allowed incoming pairing from all %d discovered peer(s) for %v\n", len(peers), windowDuration) + + // Display our node ID for others to select + fmt.Printf("\nšŸ“¢ Your node ID: %s\n", host.ID().String()) + fmt.Println("Share this ID with others so they can select you for pairing.") + fmt.Println("Waiting for incoming pairing requests...") + + // Wait for pairing to complete + return waitForIncomingPairing(ctx, host, syncService, nil, windowDuration) +} + +// waitForPairing waits for pairing to complete with selected peers +func waitForPairing(ctx context.Context, host host.Host, syncService *p2p.SyncService, selectedPeers []peer.ID, windowDuration time.Duration) error { + timeout := time.After(windowDuration) + pairedCount := 0 + + for { + select { + case <-timeout: + fmt.Printf("\nāŒ› Pairing window expired after %v\n", windowDuration) + fmt.Printf("Successfully paired with %d peer(s)\n", pairedCount) + return nil + + case <-ctx.Done(): + fmt.Printf("\nPairing interrupted. Successfully paired with %d peer(s)\n", pairedCount) + return nil + + default: + // Check if any selected peers have been successfully paired + for _, peerID := range selectedPeers { + if syncService.HasPeer(peerID) { + pairedCount++ + fmt.Printf("āœ… Successfully paired with peer: %s\n", peerID.String()) + + // Remove from selected list to avoid counting twice + for i, p := range selectedPeers { + if p == peerID { + selectedPeers = append(selectedPeers[:i], selectedPeers[i+1:]...) + break + } + } + } + } + + // If all peers are paired, we're done + if len(selectedPeers) == 0 { + fmt.Printf("\nšŸŽ‰ All selected peers successfully paired!\n") + return nil + } + + time.Sleep(1 * time.Second) + } + } +} + +// waitForIncomingPairing waits for incoming pairing requests +func waitForIncomingPairing(ctx context.Context, host host.Host, syncService *p2p.SyncService, allowedPeers []peer.ID, windowDuration time.Duration) error { + timeout := time.After(windowDuration) + pairedCount := 0 + + for { + select { + case <-timeout: + fmt.Printf("\nāŒ› Pairing window expired after %v\n", windowDuration) + fmt.Printf("Successfully paired with %d peer(s)\n", pairedCount) + return nil + + case <-ctx.Done(): + fmt.Printf("\nPairing interrupted. Successfully paired with %d peer(s)\n", pairedCount) + return nil + + default: + // Check if any allowed peers have been successfully paired + if allowedPeers != nil { + for _, peerID := range allowedPeers { + if syncService.HasPeer(peerID) { + pairedCount++ + fmt.Printf("āœ… Successfully paired with peer: %s\n", peerID.String()) + + // Remove from allowed list to avoid counting twice + for i, p := range allowedPeers { + if p == peerID { + allowedPeers = append(allowedPeers[:i], allowedPeers[i+1:]...) + break + } + } + } + } + } else { + // In allow-all mode, count all trusted peers + // This is a simplified check - in practice you'd want to track which ones were added during this session + pairedCount = len(syncService.TrustedPeers()) + } + + time.Sleep(1 * time.Second) + } + } +} diff --git a/cmd/sync.go b/cmd/sync.go index 6e60b97..64f1378 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -37,9 +37,15 @@ var syncCmd = &cobra.Command{ This command syncs your vault with another vault, either by auto-discovering peers on the local network or by connecting to a specified peer address. +For selective key exchange (recommended for larger networks), use 'sietch pair' +to establish trust relationships before syncing. This provides fine-grained +control over which peers you exchange keys with. + Examples: sietch sync # Auto-discover and sync with peers - sietch sync /ip4/192.168.1.5/tcp/4001/p2p/QmPeerID # Sync with a specific peer`, + sietch sync /ip4/192.168.1.5/tcp/4001/p2p/QmPeerID # Sync with a specific peer + sietch pair --select # Establish selective trust relationships + sietch discover --select # Discover and select peers for pairing`, RunE: func(cmd *cobra.Command, args []string) error { // Create a context with cancellation ctx, cancel := context.WithCancel(context.Background()) @@ -155,7 +161,26 @@ Examples: } if !trusted { - // If not automatically trusted, prompt user + // Check if auto-trust is disabled + if !syncService.IsAutoTrustEnabled() { + fmt.Printf("\nāš ļø Peer not trusted and auto-trust is disabled!\n") + fmt.Printf("Peer ID: %s\n", info.ID.String()) + + fingerprint, err := syncService.GetPeerFingerprint(info.ID) + if err == nil { + fmt.Printf("Fingerprint: %s\n", fingerprint) + } + + fmt.Println("\nTo establish trust with this peer, use one of these methods:") + fmt.Println("1. Run 'sietch pair --select' to interactively select peers for pairing") + fmt.Println("2. Run 'sietch pair --allow-from ' to allow this specific peer") + fmt.Println("3. Run 'sietch discover --select' to discover and select peers") + fmt.Println("4. Enable auto-trust in vault configuration (not recommended for large networks)") + + return fmt.Errorf("sync canceled - peer not trusted. Use 'sietch pair' to establish trust") + } + + // If auto-trust is enabled, prompt user (legacy behavior) fmt.Printf("\nāš ļø New peer detected!\n") fmt.Printf("Peer ID: %s\n", info.ID.String()) @@ -243,7 +268,26 @@ Examples: } if !trusted { - // If not automatically trusted, prompt user + // Check if auto-trust is disabled + if !syncService.IsAutoTrustEnabled() { + fmt.Printf("\nāš ļø Peer not trusted and auto-trust is disabled!\n") + fmt.Printf("Peer ID: %s\n", peerInfo.ID.String()) + + fingerprint, err := syncService.GetPeerFingerprint(peerInfo.ID) + if err == nil { + fmt.Printf("Fingerprint: %s\n", fingerprint) + } + + fmt.Println("\nTo establish trust with this peer, use one of these methods:") + fmt.Println("1. Run 'sietch pair --select' to interactively select peers for pairing") + fmt.Println("2. Run 'sietch pair --allow-from ' to allow this specific peer") + fmt.Println("3. Run 'sietch discover --select' to discover and select peers") + fmt.Println("4. Enable auto-trust in vault configuration (not recommended for large networks)") + + return fmt.Errorf("sync canceled - peer not trusted. Use 'sietch pair' to establish trust") + } + + // If auto-trust is enabled, prompt user (legacy behavior) fmt.Printf("\nāš ļø New peer detected!\n") fmt.Printf("Peer ID: %s\n", peerInfo.ID.String()) diff --git a/internal/config/vault.go b/internal/config/vault.go index 079cbe8..0d6e211 100644 --- a/internal/config/vault.go +++ b/internal/config/vault.go @@ -106,11 +106,12 @@ type SyncConfig struct { // RSAConfig contains RSA key configuration for sync operations type RSAConfig struct { - KeySize int `yaml:"key_size"` - PublicKeyPath string `yaml:"public_key_path,omitempty"` - PrivateKeyPath string `yaml:"private_key_path,omitempty"` - Fingerprint string `yaml:"fingerprint,omitempty"` - TrustedPeers []TrustedPeer `yaml:"trusted_peers,omitempty"` + KeySize int `yaml:"key_size"` + PublicKeyPath string `yaml:"public_key_path,omitempty"` + PrivateKeyPath string `yaml:"private_key_path,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + TrustedPeers []TrustedPeer `yaml:"trusted_peers,omitempty"` + AutoTrustAllPeers *bool `yaml:"auto_trust_all_peers,omitempty"` // nil means use default behavior } // TrustedPeer stores information about a trusted peer @@ -243,11 +244,13 @@ func BuildVaultConfigWithDeduplication( config.Sync.KnownPeers = []string{} // Initialize as empty array // Initialize RSA config for sync with defaults + autoTrustDefault := true // Default to true for backward compatibility config.Sync.RSA = &RSAConfig{ - KeySize: 4096, - PublicKeyPath: filepath.Join(".sietch", "sync", "sync_public.pem"), - PrivateKeyPath: filepath.Join(".sietch", "sync", "sync_private.pem"), - TrustedPeers: []TrustedPeer{}, + KeySize: 4096, + PublicKeyPath: filepath.Join(".sietch", "sync", "sync_public.pem"), + PrivateKeyPath: filepath.Join(".sietch", "sync", "sync_private.pem"), + TrustedPeers: []TrustedPeer{}, + AutoTrustAllPeers: &autoTrustDefault, } // Set advanced sync settings @@ -307,11 +310,13 @@ func BuildDefaultVaultConfig(vaultID, vaultName, keyPath string) VaultConfig { // Ensure default RSA configuration is set if config.Sync.RSA == nil { + autoTrustDefault := true // Default to true for backward compatibility config.Sync.RSA = &RSAConfig{ - KeySize: 4096, - PublicKeyPath: filepath.Join(".sietch", "sync", "sync_public.pem"), - PrivateKeyPath: filepath.Join(".sietch", "sync", "sync_private.pem"), - TrustedPeers: []TrustedPeer{}, + KeySize: 4096, + PublicKeyPath: filepath.Join(".sietch", "sync", "sync_public.pem"), + PrivateKeyPath: filepath.Join(".sietch", "sync", "sync_private.pem"), + TrustedPeers: []TrustedPeer{}, + AutoTrustAllPeers: &autoTrustDefault, } } diff --git a/internal/p2p/sync.go b/internal/p2p/sync.go index adbbd59..55bdc37 100644 --- a/internal/p2p/sync.go +++ b/internal/p2p/sync.go @@ -38,15 +38,19 @@ const ( // SyncService handles vault synchronization type SyncService struct { - host host.Host - vaultMgr *config.Manager - privateKey *rsa.PrivateKey - publicKey *rsa.PublicKey - rsaConfig *config.RSAConfig - trustedPeers map[peer.ID]*PeerInfo - vaultConfig *config.VaultConfig - trustAllPeers bool // New flag to automatically trust all peers - Verbose bool // Enable verbose debug output + host host.Host + vaultMgr *config.Manager + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey + rsaConfig *config.RSAConfig + trustedPeers map[peer.ID]*PeerInfo + vaultConfig *config.VaultConfig + trustAllPeers bool // Legacy flag - use autoTrustAllPeers instead + autoTrustAllPeers bool // New flag to automatically trust all peers + pendingOutgoingPeers map[peer.ID]time.Time // Peers we want to pair with + pendingIncomingAllowed map[peer.ID]time.Time // Peers allowed to pair with us + pairingWindow time.Duration // TTL for pending pairs + Verbose bool // Enable verbose debug output } // PeerInfo contains information about a trusted peer @@ -99,15 +103,25 @@ func NewSecureSyncService( return nil, fmt.Errorf("failed to load vault configuration: %w", err) } + // Determine auto-trust behavior from config + autoTrustAllPeers := true // Default to true for backward compatibility + if rsaConfig != nil && rsaConfig.AutoTrustAllPeers != nil { + autoTrustAllPeers = *rsaConfig.AutoTrustAllPeers + } + s := &SyncService{ - host: h, - vaultMgr: vm, - privateKey: privateKey, - publicKey: publicKey, - rsaConfig: rsaConfig, - trustedPeers: make(map[peer.ID]*PeerInfo), - vaultConfig: vaultConfig, - trustAllPeers: true, // Trust all peers by default + host: h, + vaultMgr: vm, + privateKey: privateKey, + publicKey: publicKey, + rsaConfig: rsaConfig, + trustedPeers: make(map[peer.ID]*PeerInfo), + vaultConfig: vaultConfig, + trustAllPeers: autoTrustAllPeers, // Legacy flag - kept for backward compatibility + autoTrustAllPeers: autoTrustAllPeers, // Use config value + pendingOutgoingPeers: make(map[peer.ID]time.Time), + pendingIncomingAllowed: make(map[peer.ID]time.Time), + pairingWindow: 5 * time.Minute, // Default 5 minute pairing window } // Load trusted peers from config @@ -186,6 +200,16 @@ func (s *SyncService) handleKeyExchange(stream network.Stream) { return } + // Check if incoming pairing is allowed for this peer + peerID := stream.Conn().RemotePeer() + if !s.IsPairingAllowed(peerID) { + fmt.Printf("Rejecting key exchange from peer %s: pairing not allowed\n", peerID.String()) + // Send error response + errorResponse := "Pairing not allowed. Peer must be explicitly allowed to pair." + _, _ = stream.Write([]byte(errorResponse)) + return + } + // Use connection deadline instead of separate read/write deadlines _ = stream.SetReadDeadline(time.Now().Add(30 * time.Second)) _ = stream.SetWriteDeadline(time.Now().Add(30 * time.Second)) @@ -278,7 +302,6 @@ func (s *SyncService) handleKeyExchange(stream network.Stream) { } // Store peer info automatically - peerID := stream.Conn().RemotePeer() s.trustedPeers[peerID] = &PeerInfo{ ID: peerID, PublicKey: peerPubKey, @@ -286,6 +309,10 @@ func (s *SyncService) handleKeyExchange(stream network.Stream) { TrustedSince: time.Now(), } + // Clear from pending maps since pairing is now complete + delete(s.pendingIncomingAllowed, peerID) + delete(s.pendingOutgoingPeers, peerID) + fmt.Printf("Key exchange completed with peer %s (fingerprint: %s)\n", peerID.String(), fingerprint) } @@ -543,9 +570,14 @@ func (s *SyncService) decryptLargeData(data []byte) []byte { // VerifyAndExchangeKeys performs key exchange with a peer func (s *SyncService) VerifyAndExchangeKeys(ctx context.Context, peerID peer.ID) (bool, error) { + // Check if outgoing pairing is requested for this peer + if !s.IsOutgoingPairRequested(peerID) { + return false, fmt.Errorf("pairing not requested for peer %s. Use 'sietch pair' to establish trust", peerID.String()) + } + // Don't return early with trustAllPeers, just mark for later needsKeyExchange := true - autoTrust := s.trustAllPeers + autoTrust := s.autoTrustAllPeers // Check if already trusted if _, ok := s.trustedPeers[peerID]; ok { @@ -1193,3 +1225,156 @@ func (s *SyncService) HasPeer(id peer.ID) bool { _, ok := s.trustedPeers[id] return ok } + +// Pairing Management Methods + +// AllowIncomingPair grants permission for a peer to pair with us +func (s *SyncService) AllowIncomingPair(peerID peer.ID, until time.Time) { + if s == nil { + return + } + s.pendingIncomingAllowed[peerID] = until + if s.Verbose { + fmt.Printf("Allowed incoming pairing from peer %s until %v\n", peerID.String(), until) + } +} + +// RequestPair requests to pair with a specific peer +func (s *SyncService) RequestPair(peerID peer.ID, until time.Time) { + if s == nil { + return + } + s.pendingOutgoingPeers[peerID] = until + if s.Verbose { + fmt.Printf("Requested pairing with peer %s until %v\n", peerID.String(), until) + } +} + +// ClearExpiredPairs removes expired entries from pending maps +func (s *SyncService) ClearExpiredPairs() { + if s == nil { + return + } + now := time.Now() + + // Clear expired outgoing pairs + for peerID, until := range s.pendingOutgoingPeers { + if now.After(until) { + delete(s.pendingOutgoingPeers, peerID) + if s.Verbose { + fmt.Printf("Cleared expired outgoing pair request for peer %s\n", peerID.String()) + } + } + } + + // Clear expired incoming pairs + for peerID, until := range s.pendingIncomingAllowed { + if now.After(until) { + delete(s.pendingIncomingAllowed, peerID) + if s.Verbose { + fmt.Printf("Cleared expired incoming pair permission for peer %s\n", peerID.String()) + } + } + } +} + +// IsPairingAllowed checks if a peer is allowed to pair with us +func (s *SyncService) IsPairingAllowed(peerID peer.ID) bool { + if s == nil { + return false + } + + // If auto-trust is enabled, allow all + if s.autoTrustAllPeers { + return true + } + + // Check if peer is already trusted + if _, ok := s.trustedPeers[peerID]; ok { + return true + } + + // Check if peer is in incoming allowed list and not expired + if until, ok := s.pendingIncomingAllowed[peerID]; ok { + if time.Now().Before(until) { + return true + } + // Clean up expired entry + delete(s.pendingIncomingAllowed, peerID) + } + + return false +} + +// IsOutgoingPairRequested checks if we have requested to pair with a peer +func (s *SyncService) IsOutgoingPairRequested(peerID peer.ID) bool { + if s == nil { + return false + } + + // If auto-trust is enabled, allow all + if s.autoTrustAllPeers { + return true + } + + // Check if peer is already trusted + if _, ok := s.trustedPeers[peerID]; ok { + return true + } + + // Check if peer is in outgoing request list and not expired + if until, ok := s.pendingOutgoingPeers[peerID]; ok { + if time.Now().Before(until) { + return true + } + // Clean up expired entry + delete(s.pendingOutgoingPeers, peerID) + } + + return false +} + +// SetAutoTrustAllPeers sets the auto-trust behavior +func (s *SyncService) SetAutoTrustAllPeers(enabled bool) { + if s == nil { + return + } + s.autoTrustAllPeers = enabled + s.trustAllPeers = enabled // Keep legacy flag in sync + if s.Verbose { + fmt.Printf("Auto-trust all peers: %v\n", enabled) + } +} + +// SetPairingWindow sets the pairing window duration +func (s *SyncService) SetPairingWindow(duration time.Duration) { + if s == nil { + return + } + s.pairingWindow = duration + if s.Verbose { + fmt.Printf("Pairing window set to: %v\n", duration) + } +} + +// TrustedPeers returns a copy of the trusted peers map +func (s *SyncService) TrustedPeers() map[peer.ID]*PeerInfo { + if s == nil { + return make(map[peer.ID]*PeerInfo) + } + + // Return a copy to prevent external modification + result := make(map[peer.ID]*PeerInfo) + for k, v := range s.trustedPeers { + result[k] = v + } + return result +} + +// IsAutoTrustEnabled returns whether auto-trust is enabled +func (s *SyncService) IsAutoTrustEnabled() bool { + if s == nil { + return false + } + return s.autoTrustAllPeers +} diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 6784ee1..c0d6f3f 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/libp2p/go-libp2p/core/peer" "github.com/manifoldco/promptui" "github.com/substantialcattle5/sietch/internal/chunk" @@ -140,3 +141,203 @@ func getEncryptionDescription(enc config.EncryptionConfig) string { return desc } + +// PeerSelectionItem represents a peer in the selection list +type PeerSelectionItem struct { + PeerID peer.ID + Addresses []string + Selected bool +} + +// SelectPeersInteractively allows users to select multiple peers from a list +func SelectPeersInteractively(peers []peer.AddrInfo) ([]peer.ID, error) { + if len(peers) == 0 { + fmt.Println("No peers available for selection.") + return []peer.ID{}, nil + } + + // Convert to selection items + items := make([]PeerSelectionItem, len(peers)) + for i, p := range peers { + addresses := make([]string, len(p.Addrs)) + for j, addr := range p.Addrs { + addresses[j] = addr.String() + } + items[i] = PeerSelectionItem{ + PeerID: p.ID, + Addresses: addresses, + Selected: false, + } + } + + fmt.Printf("\nšŸ” Found %d peer(s) on the network:\n", len(peers)) + fmt.Println(strings.Repeat("=", 50)) + + // Display peer list + for i, item := range items { + fmt.Printf("%d. %s\n", i+1, item.PeerID.String()) + for _, addr := range item.Addresses { + fmt.Printf(" └─ %s\n", addr) + } + fmt.Println() + } + + // Get selection from user + selectionPrompt := promptui.Prompt{ + Label: fmt.Sprintf("Select peers to pair with (1-%d, comma-separated, or 'all' for all peers)", len(peers)), + Validate: func(input string) error { + if input == "" { + return errors.New("please select at least one peer") + } + return nil + }, + } + + input, err := selectionPrompt.Run() + if err != nil { + return nil, fmt.Errorf("selection prompt failed: %w", err) + } + + // Parse selection + var selectedPeers []peer.ID + if strings.ToLower(input) == "all" { + // Select all peers + for _, item := range items { + selectedPeers = append(selectedPeers, item.PeerID) + } + } else { + // Parse comma-separated indices + indices := strings.Split(input, ",") + for _, indexStr := range indices { + indexStr = strings.TrimSpace(indexStr) + var index int + if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil { + return nil, fmt.Errorf("invalid selection '%s': %w", indexStr, err) + } + if index < 1 || index > len(peers) { + return nil, fmt.Errorf("selection %d is out of range (1-%d)", index, len(peers)) + } + selectedPeers = append(selectedPeers, items[index-1].PeerID) + } + } + + // Remove duplicates + uniquePeers := make([]peer.ID, 0, len(selectedPeers)) + seen := make(map[peer.ID]bool) + for _, peerID := range selectedPeers { + if !seen[peerID] { + uniquePeers = append(uniquePeers, peerID) + seen[peerID] = true + } + } + + fmt.Printf("āœ… Selected %d peer(s) for pairing:\n", len(uniquePeers)) + for _, peerID := range uniquePeers { + fmt.Printf(" • %s\n", peerID.String()) + } + + return uniquePeers, nil +} + +// PromptForPairingWindow asks user for pairing window duration +func PromptForPairingWindow() (int, error) { + windowPrompt := promptui.Prompt{ + Label: "Pairing window duration in minutes", + Default: "5", + Validate: func(input string) error { + var minutes int + if _, err := fmt.Sscanf(input, "%d", &minutes); err != nil { + return errors.New("please enter a valid number of minutes") + } + if minutes < 1 || minutes > 60 { + return errors.New("pairing window must be between 1 and 60 minutes") + } + return nil + }, + } + + input, err := windowPrompt.Run() + if err != nil { + return 0, fmt.Errorf("pairing window prompt failed: %w", err) + } + + var minutes int + fmt.Sscanf(input, "%d", &minutes) + return minutes, nil +} + +// PromptForIncomingPeers asks user which peers to allow for incoming pairing +func PromptForIncomingPeers(peers []peer.AddrInfo) ([]peer.ID, error) { + if len(peers) == 0 { + fmt.Println("No peers available for incoming pairing permission.") + return []peer.ID{}, nil + } + + fmt.Printf("\nšŸ” Grant incoming pairing permission to %d peer(s):\n", len(peers)) + fmt.Println(strings.Repeat("=", 50)) + + // Display peer list + for i, p := range peers { + fmt.Printf("%d. %s\n", i+1, p.ID.String()) + for _, addr := range p.Addrs { + fmt.Printf(" └─ %s\n", addr.String()) + } + fmt.Println() + } + + // Get selection from user + selectionPrompt := promptui.Prompt{ + Label: fmt.Sprintf("Allow incoming pairing from peers (1-%d, comma-separated, or 'all' for all peers)", len(peers)), + Validate: func(input string) error { + if input == "" { + return errors.New("please select at least one peer") + } + return nil + }, + } + + input, err := selectionPrompt.Run() + if err != nil { + return nil, fmt.Errorf("incoming peer selection prompt failed: %w", err) + } + + // Parse selection (same logic as SelectPeersInteractively) + var selectedPeers []peer.ID + if strings.ToLower(input) == "all" { + // Select all peers + for _, p := range peers { + selectedPeers = append(selectedPeers, p.ID) + } + } else { + // Parse comma-separated indices + indices := strings.Split(input, ",") + for _, indexStr := range indices { + indexStr = strings.TrimSpace(indexStr) + var index int + if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil { + return nil, fmt.Errorf("invalid selection '%s': %w", indexStr, err) + } + if index < 1 || index > len(peers) { + return nil, fmt.Errorf("selection %d is out of range (1-%d)", index, len(peers)) + } + selectedPeers = append(selectedPeers, peers[index-1].ID) + } + } + + // Remove duplicates + uniquePeers := make([]peer.ID, 0, len(selectedPeers)) + seen := make(map[peer.ID]bool) + for _, peerID := range selectedPeers { + if !seen[peerID] { + uniquePeers = append(uniquePeers, peerID) + seen[peerID] = true + } + } + + fmt.Printf("āœ… Granted incoming pairing permission to %d peer(s):\n", len(uniquePeers)) + for _, peerID := range uniquePeers { + fmt.Printf(" • %s\n", peerID.String()) + } + + return uniquePeers, nil +}