summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2025-11-25 20:21:38 +0100
committerGitHub <noreply@github.com>2025-11-25 20:21:38 +0100
commit5d35a38e32b6996ae115b4d59099b527104350c2 (patch)
tree5c676d7ae88773711c2ac91c32eee5fb03110375
parent66aeaef3c5844313c60a5e97e01b5531a43c5727 (diff)
downloadnoshitalk-5d35a38e32b6996ae115b4d59099b527104350c2.tar.gz
noshitalk-5d35a38e32b6996ae115b4d59099b527104350c2.tar.xz
noshitalk-5d35a38e32b6996ae115b4d59099b527104350c2.zip
Delete noshitalk_cli-client.go
-rw-r--r--noshitalk_cli-client.go869
1 files changed, 0 insertions, 869 deletions
diff --git a/noshitalk_cli-client.go b/noshitalk_cli-client.go
deleted file mode 100644
index 4d4eb14..0000000
--- a/noshitalk_cli-client.go
+++ /dev/null
@@ -1,869 +0,0 @@
-package main
-
-import (
- "bufio"
- "crypto/aes"
- "crypto/cipher"
- "crypto/ecdh"
- "crypto/hmac"
- cryptorand "crypto/rand"
- "crypto/sha256"
- "encoding/binary"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "math/rand"
- "net"
- "net/url"
- "os"
- "os/signal"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "syscall"
- "time"
-
- "github.com/awnumar/memguard"
- "golang.org/x/net/proxy"
-)
-
-type CLIClient struct {
- conn net.Conn
- gcm cipher.AEAD
- sharedSecret *memguard.Enclave
- connected bool
- serverAddr string
- quit chan struct{}
- mu sync.Mutex
- autoReconnect bool
- lastServer string
- onlineUsers []string
-
- // Persistent identity
- privateKey *ecdh.PrivateKey
- identity string
-}
-
-type Message struct {
- From string `json:"from"`
- To string `json:"to,omitempty"`
- Content string `json:"content"`
- Time string `json:"time"`
- Type string `json:"type"`
-}
-
-type UserListMessage struct {
- Type string `json:"type"`
- Users []string `json:"users"`
- Time string `json:"time"`
-}
-
-var (
- version = "0.8-identity"
- onionRegex = regexp.MustCompile(`^[a-z2-7]{56}\.onion(:[0-9]{1,5})?$`)
-)
-
-const (
- messageBlockSize = 256
- minRandomDelay = 50
- maxRandomDelay = 200
-)
-
-func main() {
- memguard.CatchInterrupt()
- defer memguard.Purge()
-
- fmt.Printf("🔐 NoshiTalk CLI Client v%s\n", version)
- fmt.Printf("💻 Lightweight Command Line Interface\n")
- fmt.Printf("🧅 Tor-Only Mode - No Clearnet Connections\n")
- fmt.Printf("🔒 ECDH + HMAC + AES-GCM Encryption\n\n")
-
- client := &CLIClient{
- quit: make(chan struct{}),
- autoReconnect: true,
- onlineUsers: []string{},
- }
-
- // Initialize persistent identity
- if err := client.initializeIdentity(); err != nil {
- fmt.Printf("❌ Failed to initialize identity: %v\n", err)
- fmt.Printf(" Continuing with temporary identity...\n\n")
- }
-
- // Setup signal handling
- sigChan := make(chan os.Signal, 1)
- signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
- go func() {
- <-sigChan
- fmt.Printf("\n🛑 Shutting down securely...\n")
- client.disconnect()
- memguard.Purge()
- os.Exit(0)
- }()
-
- // Start auto-reconnect goroutine
- go client.autoReconnectLoop()
-
- client.run()
-}
-
-func (c *CLIClient) run() {
- scanner := bufio.NewScanner(os.Stdin)
-
- fmt.Printf("📡 Enter Tor Hidden Service address (.onion only)\n")
- fmt.Printf("💡 Example: abcd1234...xyz9876.onion:8083\n")
- fmt.Printf("⚠️ Clearnet addresses blocked by design\n\n")
-
- for {
- if !c.connected {
- fmt.Print("🧅 .onion address: ")
- if !scanner.Scan() {
- break
- }
- c.serverAddr = strings.TrimSpace(scanner.Text())
-
- if c.serverAddr == "" {
- fmt.Printf("❌ Address required\n\n")
- continue
- }
-
- // Validate .onion address
- if err := validateOnionAddress(c.serverAddr); err != nil {
- fmt.Printf("❌ Invalid address: %v\n\n", err)
- continue
- }
-
- c.lastServer = c.serverAddr
- fmt.Printf("⏳ Connecting to %s via Tor...\n", c.serverAddr)
-
- if err := c.connect(); err != nil {
- fmt.Printf("❌ Connection failed: %v\n", err)
- fmt.Printf("💡 Will retry automatically if auto-reconnect is enabled\n\n")
- continue
- }
- }
-
- fmt.Print("💬 > ")
- if !scanner.Scan() {
- break
- }
-
- message := strings.TrimSpace(scanner.Text())
- if message == "" {
- continue
- }
-
- // Handle commands
- switch message {
- case "/quit", "/exit", "/q":
- c.autoReconnect = false
- c.disconnect()
- fmt.Println("👋 Goodbye!")
- return
- case "/disconnect", "/d":
- c.disconnect()
- continue
- case "/help", "/h", "/?":
- c.showHelp()
- continue
- case "/status", "/s":
- c.showStatus()
- continue
- case "/reconnect", "/r":
- if !c.connected && c.lastServer != "" {
- c.serverAddr = c.lastServer
- fmt.Printf("🔄 Reconnecting to %s...\n", c.serverAddr)
- c.connect()
- }
- continue
- case "/auto on":
- c.autoReconnect = true
- fmt.Printf("✅ Auto-reconnect enabled\n")
- continue
- case "/auto off":
- c.autoReconnect = false
- fmt.Printf("❌ Auto-reconnect disabled\n")
- continue
- case "/clear", "/cls":
- fmt.Print("\033[H\033[2J")
- continue
- case "/users", "/u":
- c.showUsers()
- continue
- }
-
- // Handle /pm command
- if len(message) > 4 && message[:4] == "/pm " {
- if !c.connected {
- fmt.Printf("❌ Not connected. Use /reconnect or enter server address\n")
- continue
- }
- // Send directly - server will parse it
- if err := c.sendMessage(message); err != nil {
- fmt.Printf("❌ Send error: %v\n", err)
- c.disconnect()
- }
- continue
- }
-
- // Send regular message
- if !c.connected {
- fmt.Printf("❌ Not connected. Use /reconnect or enter server address\n")
- continue
- }
-
- if err := c.sendMessage(message); err != nil {
- fmt.Printf("❌ Send error: %v\n", err)
- c.disconnect()
- }
- }
-
- c.disconnect()
-}
-
-func validateOnionAddress(addr string) error {
- if addr == "" {
- return fmt.Errorf("address cannot be empty")
- }
-
- if !strings.Contains(addr, ".onion") {
- return fmt.Errorf("only Tor .onion addresses allowed - no clearnet connections")
- }
-
- if !onionRegex.MatchString(addr) {
- return fmt.Errorf("invalid .onion address format (must be v3: 56 chars + .onion:port)")
- }
-
- return nil
-}
-
-func (c *CLIClient) connect() error {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- if c.connected {
- return nil
- }
-
- // Only Tor connections allowed
- fmt.Printf("🧅 Routing through Tor network...\n")
- conn, err := c.connectThroughTor()
- if err != nil {
- return err
- }
-
- c.conn = conn
- return c.setupEncryption()
-}
-
-func (c *CLIClient) connectThroughTor() (net.Conn, error) {
- fmt.Printf("🧅 Initializing Tor connection...\n")
-
- // Test Tor proxy
- if err := c.testTorProxy(); err != nil {
- return nil, fmt.Errorf("Tor proxy not available: %v", err)
- }
-
- // Try multiple proxy addresses
- proxyURLs := []string{
- "socks5://127.0.0.1:9050",
- "socks5://localhost:9050",
- "socks5://127.0.0.1:9150", // Tor Browser
- }
-
- var conn net.Conn
- var lastErr error
-
- for _, proxyURL := range proxyURLs {
- fmt.Printf("🔗 Trying proxy %s...\n", proxyURL)
-
- torProxyUrl, err := url.Parse(proxyURL)
- if err != nil {
- lastErr = err
- continue
- }
-
- baseDialer := &net.Dialer{
- Timeout: 90 * time.Second,
- KeepAlive: 30 * time.Second,
- }
-
- dialer, err := proxy.FromURL(torProxyUrl, baseDialer)
- if err != nil {
- lastErr = err
- continue
- }
-
- fmt.Printf("⏳ Connecting to %s (may take up to 90 seconds)...\n", c.serverAddr)
-
- conn, err = dialer.Dial("tcp", c.serverAddr)
- if err != nil {
- lastErr = err
- fmt.Printf("❌ Failed with %s: %v\n", proxyURL, err)
- continue
- }
-
- fmt.Printf("✅ Connected through %s\n", proxyURL)
- break
- }
-
- if conn == nil {
- return nil, fmt.Errorf("all Tor proxy attempts failed: %v", lastErr)
- }
-
- fmt.Printf("✅ TCP connection through Tor established\n")
- fmt.Printf("✅ Tor circuit complete (3-layer encryption)\n")
- fmt.Printf("🔒 Proceeding to ECDH handshake...\n")
-
- return conn, nil
-}
-
-func (c *CLIClient) testTorProxy() error {
- conn, err := net.DialTimeout("tcp", "127.0.0.1:9050", 2*time.Second)
- if err != nil {
- return fmt.Errorf("Tor SOCKS proxy not responding on port 9050")
- }
- conn.Close()
-
- fmt.Printf("✅ Tor proxy detected and responsive\n")
- return nil
-}
-
-func (c *CLIClient) setupEncryption() error {
- fmt.Printf("🔐 Establishing end-to-end encryption...\n")
-
- // X25519 curve for key exchange
- curve := ecdh.X25519()
-
- // Use persistent identity key (or generate temporary if not available)
- var privateKey *ecdh.PrivateKey
- if c.privateKey != nil {
- privateKey = c.privateKey
- fmt.Printf("🔑 Using persistent identity: %s\n", c.identity)
- } else {
- // Fallback: generate temporary key
- var err error
- privateKey, err = curve.GenerateKey(cryptorand.Reader)
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("key generation failed: %v", err)
- }
- fmt.Printf("⚠️ Using temporary identity (will change on reconnect)\n")
- }
-
- privateKeyBuffer := memguard.NewBufferFromBytes(privateKey.Bytes())
- defer privateKeyBuffer.Destroy()
-
- // Send public key (this identifies us to the server)
- publicKey := privateKey.PublicKey()
- publicKeyBytes := publicKey.Bytes()
-
- c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
- totalSent := 0
- for totalSent < len(publicKeyBytes) {
- n, err := c.conn.Write(publicKeyBytes[totalSent:])
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("failed to send public key: %v", err)
- }
- totalSent += n
- }
- c.conn.SetWriteDeadline(time.Time{})
-
- // Receive server's public key
- serverPubKeyBytes := make([]byte, 32)
- if _, err := io.ReadFull(c.conn, serverPubKeyBytes); err != nil {
- c.conn.Close()
- return fmt.Errorf("failed to receive server public key: %v", err)
- }
-
- serverPublicKey, err := curve.NewPublicKey(serverPubKeyBytes)
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("invalid server public key: %v", err)
- }
-
- // Calculate shared secret
- sharedSecret, err := privateKey.ECDH(serverPublicKey)
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("ECDH failed: %v", err)
- }
-
- // Use sharedSecret directly, seal into memguard after we're done
- // HMAC Mutual Authentication
- fmt.Printf("🔐 Starting HMAC mutual authentication...\n")
- if err := c.performMutualAuth(sharedSecret); err != nil {
- c.conn.Close()
- return fmt.Errorf("authentication failed: %v", err)
- }
- fmt.Printf("✅ Mutual authentication successful\n")
-
- // Setup AES-GCM
- block, err := aes.NewCipher(sharedSecret[:32])
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("AES cipher creation failed: %v", err)
- }
-
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- c.conn.Close()
- return fmt.Errorf("GCM cipher creation failed: %v", err)
- }
-
- // Now seal sharedSecret into memguard (this zeros the original)
- sharedSecretBuffer := memguard.NewBufferFromBytes(sharedSecret)
- defer sharedSecretBuffer.Destroy()
-
- c.gcm = gcm
- c.sharedSecret = sharedSecretBuffer.Seal()
- c.connected = true
-
- // Success messages
- fmt.Printf("✅ Connected to %s\n", c.serverAddr)
- fmt.Printf("🔐 End-to-end encryption active (AES-256-GCM)\n")
- fmt.Printf("🧅 Anonymous Tor routing active\n")
- fmt.Printf("🔒 ECDH + HMAC + AES-GCM protection\n")
- fmt.Printf("💬 Ready for secure messaging\n")
- fmt.Printf("ℹ️ Type /help for commands\n\n")
-
- // Start receiving messages
- go c.receiveMessages()
-
- return nil
-}
-
-func (c *CLIClient) performMutualAuth(sharedSecret []byte) error {
- c.conn.SetDeadline(time.Now().Add(30 * time.Second))
- defer c.conn.SetDeadline(time.Time{})
-
- // Step 1: Receive server challenge
- serverChallenge := make([]byte, 32)
- if _, err := io.ReadFull(c.conn, serverChallenge); err != nil {
- return fmt.Errorf("receive server challenge failed: %v", err)
- }
-
- // Step 2: Compute HMAC response to server's challenge
- h := hmac.New(sha256.New, sharedSecret)
- h.Write(serverChallenge)
- clientResponse := h.Sum(nil)
-
- // Step 3: Generate client's own challenge
- clientChallenge := make([]byte, 32)
- if _, err := cryptorand.Read(clientChallenge); err != nil {
- return fmt.Errorf("challenge generation failed: %v", err)
- }
-
- // Step 4: Send client response + client challenge
- if _, err := c.conn.Write(clientResponse); err != nil {
- return fmt.Errorf("send client response failed: %v", err)
- }
- if _, err := c.conn.Write(clientChallenge); err != nil {
- return fmt.Errorf("send client challenge failed: %v", err)
- }
-
- // Step 5: Receive and verify server's response
- serverResponse := make([]byte, 32)
- if _, err := io.ReadFull(c.conn, serverResponse); err != nil {
- return fmt.Errorf("receive server response failed: %v", err)
- }
-
- h.Reset()
- h.Write(clientChallenge)
- expectedServerMAC := h.Sum(nil)
-
- if !hmac.Equal(serverResponse, expectedServerMAC) {
- return fmt.Errorf("server authentication failed - HMAC mismatch")
- }
-
- return nil
-}
-
-func (c *CLIClient) disconnect() {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- if !c.connected {
- return
- }
-
- c.sendEncryptedMessage("/quit")
- time.Sleep(100 * time.Millisecond)
-
- if c.conn != nil {
- c.conn.Close()
- c.conn = nil
- }
-
- if c.sharedSecret != nil {
- // Open enclave and destroy the buffer
- buf, _ := c.sharedSecret.Open()
- if buf != nil {
- buf.Destroy()
- }
- c.sharedSecret = nil
- }
- c.gcm = nil
-
- c.connected = false
- fmt.Printf("\n📴 Disconnected from server\n")
- fmt.Printf("🔒 Keys wiped from memory\n")
-
- if c.autoReconnect {
- fmt.Printf("🔄 Auto-reconnect is enabled\n")
- }
- fmt.Printf("\n")
-}
-
-func (c *CLIClient) sendMessage(message string) error {
- return c.sendEncryptedMessage(message)
-}
-
-func padMessage(plaintext []byte) []byte {
- currentLen := len(plaintext)
- paddedLen := ((currentLen / messageBlockSize) + 1) * messageBlockSize
- padLen := paddedLen - currentLen
-
- result := make([]byte, 2+paddedLen)
- binary.BigEndian.PutUint16(result[0:2], uint16(currentLen))
- copy(result[2:], plaintext)
-
- if padLen > 0 {
- padding := make([]byte, padLen)
- cryptorand.Read(padding)
- copy(result[2+currentLen:], padding)
- }
-
- return result
-}
-
-func unpadMessage(paddedData []byte) ([]byte, error) {
- if len(paddedData) < 2 {
- return nil, fmt.Errorf("padded data too short")
- }
-
- originalLen := binary.BigEndian.Uint16(paddedData[0:2])
-
- if int(originalLen) > len(paddedData)-2 {
- return nil, fmt.Errorf("invalid padding length")
- }
-
- return paddedData[2 : 2+originalLen], nil
-}
-
-func randomDelay() {
- delay := minRandomDelay + rand.Intn(maxRandomDelay-minRandomDelay)
- time.Sleep(time.Duration(delay) * time.Millisecond)
-}
-
-func (c *CLIClient) sendEncryptedMessage(message string) error {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- if !c.connected || c.gcm == nil || c.conn == nil {
- return fmt.Errorf("not connected")
- }
-
- // Random delay (timing attack mitigation)
- randomDelay()
-
- // Pad message
- paddedMessage := padMessage([]byte(message))
-
- nonce := make([]byte, 12)
- if _, err := io.ReadFull(cryptorand.Reader, nonce); err != nil {
- return fmt.Errorf("nonce generation failed: %v", err)
- }
-
- encrypted := c.gcm.Seal(nil, nonce, paddedMessage, nil)
- data := append(nonce, encrypted...)
-
- _, err := c.conn.Write(data)
- return err
-}
-
-func (c *CLIClient) receiveMessages() {
- buf := make([]byte, 8192)
-
- for {
- c.mu.Lock()
- if !c.connected {
- c.mu.Unlock()
- return
- }
- conn := c.conn
- gcm := c.gcm
- c.mu.Unlock()
-
- // No read deadline for persistent connections
- conn.SetReadDeadline(time.Time{})
-
- n, err := conn.Read(buf)
- if err != nil {
- if err == io.EOF {
- fmt.Printf("\n📴 Server closed connection\n")
- } else {
- fmt.Printf("\n❌ Connection lost: %v\n", err)
- }
-
- c.mu.Lock()
- c.connected = false
- c.mu.Unlock()
- return
- }
-
- if n < 12 {
- continue
- }
-
- // Decrypt message
- nonce := buf[:12]
- ciphertext := buf[12:n]
-
- paddedPlaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
- if err != nil {
- fmt.Printf("\n⚠️ Failed to decrypt message\n")
- continue
- }
-
- // Remove padding
- plaintext, err := unpadMessage(paddedPlaintext)
- if err != nil {
- fmt.Printf("\n⚠️ Failed to unpad message: %v\n", err)
- continue
- }
-
- // Try to parse as UserListMessage first
- var userListMsg UserListMessage
- if err := json.Unmarshal(plaintext, &userListMsg); err == nil && userListMsg.Type == "user_list" {
- c.mu.Lock()
- c.onlineUsers = userListMsg.Users
- c.mu.Unlock()
- fmt.Printf("\r👥 Online users (%d): %s\n💬 > ", len(userListMsg.Users), strings.Join(userListMsg.Users, ", "))
- continue
- }
-
- // Try to parse as Message
- var msg Message
- if err := json.Unmarshal(plaintext, &msg); err == nil {
- timestamp := msg.Time
- if timestamp == "" {
- timestamp = time.Now().Format("15:04:05")
- }
-
- switch msg.Type {
- case "system":
- fmt.Printf("\r🔔 [%s] %s\n💬 > ", timestamp, msg.Content)
- case "private":
- if msg.To != "" {
- // Private message
- fmt.Printf("\r🔒 [%s] PM from %s: %s\n💬 > ", timestamp, msg.From, msg.Content)
- }
- case "error":
- fmt.Printf("\r❌ [%s] %s\n💬 > ", timestamp, msg.Content)
- default:
- // Regular message
- if msg.From == "" {
- fmt.Printf("\r📨 [%s] %s\n💬 > ", timestamp, msg.Content)
- } else {
- fmt.Printf("\r👤 [%s] %s: %s\n💬 > ", timestamp, msg.From, msg.Content)
- }
- }
- } else {
- // Plain message fallback
- timestamp := time.Now().Format("15:04:05")
- fmt.Printf("\r📨 [%s] %s\n💬 > ", timestamp, string(plaintext))
- }
- }
-}
-
-func (c *CLIClient) autoReconnectLoop() {
- lastAttempt := time.Now()
-
- for {
- time.Sleep(5 * time.Second)
-
- c.mu.Lock()
- shouldReconnect := !c.connected && c.autoReconnect && c.lastServer != ""
- c.mu.Unlock()
-
- if shouldReconnect {
- if time.Since(lastAttempt) < 10*time.Second {
- continue
- }
- lastAttempt = time.Now()
-
- fmt.Printf("\n🔄 Auto-reconnecting to %s...\n", c.lastServer)
- c.serverAddr = c.lastServer
- if err := c.connect(); err != nil {
- fmt.Printf("❌ Auto-reconnect failed: %v\n", err)
- fmt.Printf("⏳ Will retry in 10 seconds...\n")
- }
- }
- }
-}
-
-func (c *CLIClient) showStatus() {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- fmt.Printf("\n📊 Connection Status:\n")
- fmt.Printf(" Connected: %v\n", c.connected)
- if c.connected {
- fmt.Printf(" Server: %s\n", c.serverAddr)
- fmt.Printf(" Mode: Tor Hidden Service (Anonymous)\n")
- fmt.Printf(" Encryption: AES-256-GCM\n")
- fmt.Printf(" Key Exchange: ECDH-X25519\n")
- fmt.Printf(" Authentication: HMAC-SHA256\n")
- }
- fmt.Printf(" Auto-Reconnect: %v\n", c.autoReconnect)
- fmt.Printf(" Version: %s\n\n", version)
-}
-
-func (c *CLIClient) showHelp() {
- fmt.Printf(`
-╔════════════════════════════════════════════╗
-║ NoshiTalk CLI Commands v%s ║
-╚════════════════════════════════════════════╝
-
-Connection:
- /reconnect, /r - Reconnect to last server
- /disconnect, /d - Disconnect from server
- /status, /s - Show connection status
-
-Messaging:
- /users, /u - Show online users
- /pm user message - Send private message
-
-Settings:
- /auto on - Enable auto-reconnect
- /auto off - Disable auto-reconnect
-
-Interface:
- /clear, /cls - Clear screen
- /help, /h, /? - Show this help
- /quit, /exit, /q - Exit application
-
-Tips:
- • Only .onion addresses accepted (Tor-only mode)
- • All messages are end-to-end encrypted
- • ECDH + HMAC + AES-GCM protection
- • Auto-reconnect keeps you connected
- • Press Ctrl+C for emergency shutdown
- • Clearnet connections blocked by design
-
-Examples:
- /pm alice Hey, how are you?
- /users
-
-`, version)
-}
-
-func (c *CLIClient) showUsers() {
- c.mu.Lock()
- users := c.onlineUsers
- c.mu.Unlock()
-
- if len(users) == 0 {
- fmt.Printf("👥 No users online (or not yet received user list)\n")
- return
- }
-
- fmt.Printf("👥 Online users (%d):\n", len(users))
- for _, user := range users {
- fmt.Printf(" • %s\n", user)
- }
-}
-
-// Identity management functions
-
-func (c *CLIClient) initializeIdentity() error {
- homeDir, err := os.UserHomeDir()
- if err != nil {
- return err
- }
-
- keyPath := filepath.Join(homeDir, ".noshitalk", "identity.key")
-
- // Try to load existing key
- if _, err := os.Stat(keyPath); err == nil {
- return c.loadIdentity(keyPath)
- }
-
- // Generate new key pair
- return c.generateNewIdentity(keyPath)
-}
-
-func (c *CLIClient) generateNewIdentity(keyPath string) error {
- fmt.Printf("🔑 No identity found. Generating new anonymous identity...\n")
-
- curve := ecdh.X25519()
- privKey, err := curve.GenerateKey(cryptorand.Reader)
- if err != nil {
- return err
- }
-
- pubKey := privKey.PublicKey().Bytes()
-
- // Derive identity from public key
- hash := sha256.Sum256(pubKey)
- identity := fmt.Sprintf("anon_%s", hex.EncodeToString(hash[:8]))
-
- c.privateKey = privKey
- c.identity = identity
-
- fmt.Printf("✅ Identity created: %s\n", identity)
- fmt.Printf(" (will be saved to %s)\n\n", keyPath)
-
- // Save to disk
- return c.saveIdentity(keyPath)
-}
-
-func (c *CLIClient) loadIdentity(keyPath string) error {
- fmt.Printf("🔑 Loading identity from %s\n", keyPath)
-
- data, err := os.ReadFile(keyPath)
- if err != nil {
- return err
- }
-
- curve := ecdh.X25519()
- privKey, err := curve.NewPrivateKey(data)
- if err != nil {
- return err
- }
-
- pubKey := privKey.PublicKey().Bytes()
-
- // Derive identity
- hash := sha256.Sum256(pubKey)
- identity := fmt.Sprintf("anon_%s", hex.EncodeToString(hash[:8]))
-
- c.privateKey = privKey
- c.identity = identity
-
- fmt.Printf("✅ Identity loaded: %s\n\n", identity)
-
- return nil
-}
-
-func (c *CLIClient) saveIdentity(keyPath string) error {
- // Create directory if needed
- dir := filepath.Dir(keyPath)
- if err := os.MkdirAll(dir, 0700); err != nil {
- return err
- }
-
- // Save private key
- privKeyBytes := c.privateKey.Bytes()
-
- if err := os.WriteFile(keyPath, privKeyBytes, 0600); err != nil {
- return err
- }
-
- fmt.Printf("💾 Identity saved securely\n")
- return nil
-}