diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-10-06 10:25:04 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-06 10:25:04 +0200 |
| commit | 788d5f759d6136c1491ac64bb25b5f53fe65c4df (patch) | |
| tree | 312ff9b8e4aaefddc482251a8cb9f284f3b0a03d | |
| parent | 74f06db22b9ec8cb1591520cd7256ce15d9c4e1e (diff) | |
| download | noshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.tar.gz noshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.tar.xz noshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.zip | |
Update noshitalk-client.go
| -rw-r--r-- | noshitalk-client.go | 956 |
1 files changed, 598 insertions, 358 deletions
diff --git a/noshitalk-client.go b/noshitalk-client.go index 9512fa8..bed425c 100644 --- a/noshitalk-client.go +++ b/noshitalk-client.go @@ -4,15 +4,21 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdh" - "crypto/rand" - "crypto/tls" + "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" "syscall" "time" @@ -29,33 +35,82 @@ import ( ) type ChatClient struct { - conn net.Conn - gcm cipher.AEAD - app fyne.App - window fyne.Window - messages *widget.List - messageList []string - input *widget.Entry - status *widget.Label - connectBtn *widget.Button - serverEntry *widget.Entry - debugLog *widget.Entry - connected bool + conn net.Conn + gcm cipher.AEAD + sharedSecret *memguard.Enclave + app fyne.App + window fyne.Window + messages *widget.List + messageList []string + input *widget.Entry + status *widget.Label + connectBtn *widget.Button + serverEntry *widget.Entry + debugLog *widget.Entry + connected bool autoReconnect bool - lastServer string + lastServer string + userList *widget.List + onlineUsers []string + username string + fingerprint string + signKey *ecdh.PrivateKey + encryptKey *ecdh.PrivateKey } -var version = "0.1" +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"` +} + +type IdentityFile struct { + Version string `json:"version"` + Username string `json:"username"` + Fingerprint string `json:"fingerprint"` + SignKeyPair KeyPair `json:"signKeyPair"` + EncryptKeyPair KeyPair `json:"encryptKeyPair"` +} + +type KeyPair struct { + PrivateKey string `json:"privateKey"` + PublicKey string `json:"publicKey"` +} + +var ( + version = "1.0" + 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() - chatApp := app.NewWithID("noshitalk-client") + chatApp := app.NewWithID("noshitalk-gui") client := &ChatClient{ - app: chatApp, - messageList: []string{}, + app: chatApp, + messageList: []string{}, autoReconnect: true, + onlineUsers: []string{}, + } + + if err := client.initializeIdentity(); err != nil { + fmt.Printf("Identity initialization failed: %v\n", err) + fmt.Printf("Continuing with temporary identity\n\n") } client.createMainWindow() @@ -63,19 +118,145 @@ func main() { client.window.ShowAndRun() } +func (c *ChatClient) initializeIdentity() error { + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + keyPath := filepath.Join(homeDir, ".noshitalk", "gui-identity.noshikey") + + if _, err := os.Stat(keyPath); err == nil { + return c.loadIdentity(keyPath) + } + + return c.generateNewIdentity(keyPath) +} + +func (c *ChatClient) generateNewIdentity(keyPath string) error { + fmt.Printf("Generating new identity\n") + + curve := ecdh.X25519() + + signKey, err := curve.GenerateKey(cryptorand.Reader) + if err != nil { + return err + } + + encryptKey, err := curve.GenerateKey(cryptorand.Reader) + if err != nil { + return err + } + + signPubKey := signKey.PublicKey().Bytes() + hash := sha256.Sum256(signPubKey) + fingerprint := hex.EncodeToString(hash[:16]) + + username := fmt.Sprintf("gui_%s", fingerprint[:8]) + + c.signKey = signKey + c.encryptKey = encryptKey + c.fingerprint = fingerprint + c.username = username + + fmt.Printf("Identity: %s\n", fingerprint) + fmt.Printf("Username: %s\n", username) + + return c.saveIdentity(keyPath) +} + +func (c *ChatClient) loadIdentity(keyPath string) error { + fmt.Printf("Loading identity from %s\n", keyPath) + + data, err := os.ReadFile(keyPath) + if err != nil { + return err + } + + var identity IdentityFile + if err := json.Unmarshal(data, &identity); err != nil { + return err + } + + curve := ecdh.X25519() + + signPrivBytes, err := hex.DecodeString(identity.SignKeyPair.PrivateKey) + if err != nil { + return err + } + + encryptPrivBytes, err := hex.DecodeString(identity.EncryptKeyPair.PrivateKey) + if err != nil { + return err + } + + signKey, err := curve.NewPrivateKey(signPrivBytes) + if err != nil { + return err + } + + encryptKey, err := curve.NewPrivateKey(encryptPrivBytes) + if err != nil { + return err + } + + c.signKey = signKey + c.encryptKey = encryptKey + c.fingerprint = identity.Fingerprint + c.username = identity.Username + + fmt.Printf("Identity loaded: %s\n", identity.Fingerprint) + fmt.Printf("Username: %s\n\n", identity.Username) + + return nil +} + +func (c *ChatClient) saveIdentity(keyPath string) error { + dir := filepath.Dir(keyPath) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + + identity := IdentityFile{ + Version: "1.0", + Username: c.username, + Fingerprint: c.fingerprint, + SignKeyPair: KeyPair{ + PrivateKey: hex.EncodeToString(c.signKey.Bytes()), + PublicKey: hex.EncodeToString(c.signKey.PublicKey().Bytes()), + }, + EncryptKeyPair: KeyPair{ + PrivateKey: hex.EncodeToString(c.encryptKey.Bytes()), + PublicKey: hex.EncodeToString(c.encryptKey.PublicKey().Bytes()), + }, + } + + data, err := json.MarshalIndent(identity, "", " ") + if err != nil { + return err + } + + if err := os.WriteFile(keyPath, data, 0600); err != nil { + return err + } + + fmt.Printf("Identity saved to %s\n\n", keyPath) + return nil +} + func (c *ChatClient) createMainWindow() { - c.window = c.app.NewWindow(fmt.Sprintf("π NoshiTalk Client v%s - Maximum Security", version)) + c.window = c.app.NewWindow(fmt.Sprintf("NoshiTalk GUI v%s", version)) c.window.SetIcon(theme.ComputerIcon()) c.window.Resize(fyne.NewSize(1200, 800)) - c.status = widget.NewLabel("π΄ Disconnected - Ready to connect") + c.status = widget.NewLabel("Disconnected") c.status.TextStyle.Bold = true c.serverEntry = widget.NewEntry() - c.serverEntry.SetText("localhost:8083") - c.serverEntry.SetPlaceHolder("server:port or .onion:port") + c.serverEntry.SetText("") + c.serverEntry.SetPlaceHolder("abc...xyz.onion:8080") - c.connectBtn = widget.NewButton("π Connect to Secure Server", c.connectToServer) + c.connectBtn = widget.NewButton("Connect", c.connectToServer) c.connectBtn.Importance = widget.HighImportance c.messages = widget.NewList( @@ -83,90 +264,112 @@ func (c *ChatClient) createMainWindow() { return len(c.messageList) }, func() fyne.CanvasObject { - return widget.NewLabel("Template message") + return widget.NewLabel("Template") }, func(i widget.ListItemID, o fyne.CanvasObject) { o.(*widget.Label).SetText(c.messageList[i]) }, ) + c.userList = widget.NewList( + func() int { + return len(c.onlineUsers) + }, + func() fyne.CanvasObject { + return widget.NewButton("", nil) + }, + func(i widget.ListItemID, o fyne.CanvasObject) { + if i < len(c.onlineUsers) { + btn := o.(*widget.Button) + username := c.onlineUsers[i] + btn.SetText(username) + btn.OnTapped = func() { + c.onUserClick(username) + } + } + }, + ) + c.input = widget.NewEntry() - c.input.SetPlaceHolder("Type your secure message here...") + c.input.SetPlaceHolder("Message...") c.input.Disable() c.input.OnSubmitted = c.sendMessage - // Auto-reconnect checkbox autoReconnectCheck := widget.NewCheck("Auto-reconnect", func(checked bool) { c.autoReconnect = checked - if checked { - c.addDebugLog("π Auto-reconnect enabled") - } else { - c.addDebugLog("βΈοΈ Auto-reconnect disabled") - } }) - autoReconnectCheck.SetChecked(true) - securityPanel := widget.NewCard("π Security Status", "", + panicBtn := widget.NewButton("PANIC", c.panic) + panicBtn.Importance = widget.DangerImportance + + identityInfo := widget.NewLabel("Identity: Loading...") + if c.fingerprint != "" { + identityInfo.SetText(fmt.Sprintf("ID: %s", c.fingerprint[:16])) + } + + securityPanel := widget.NewCard("Security", "", container.NewVBox( - widget.NewLabel("β’ ECC Authentication β\nβ’ TLS 1.3 Encryption β\nβ’ Perfect Forward Secrecy β\nβ’ Zero Logging β\nβ’ Memory Protection β\nβ’ Tor Support β"), + widget.NewLabel("Tor only\nE2E encrypted\nEphemeral messages\nmemguard protected"), autoReconnectCheck, + identityInfo, + panicBtn, )) c.debugLog = widget.NewEntry() c.debugLog.MultiLine = true c.debugLog.Wrapping = fyne.TextWrapWord - c.debugLog.SetText("Ready to connect...\nUse this panel to monitor connection details.\n") - + c.debugLog.SetText("Ready\n") + + autoReconnectCheck.SetChecked(true) + debugScroll := container.NewScroll(c.debugLog) debugScroll.SetMinSize(fyne.NewSize(350, 300)) - - debugPanel := widget.NewCard("π Debug Log", "", debugScroll) - clearLogBtn := widget.NewButton("π§Ή Clear Log", func() { - c.debugLog.SetText("Debug log cleared.\n") - }) + debugPanel := widget.NewCard("Debug", "", debugScroll) - copyLogBtn := widget.NewButton("π Copy All", func() { - if c.debugLog.Text != "" { - c.window.Clipboard().SetContent(c.debugLog.Text) - c.addDebugLog("π Log copied to clipboard") - } + clearLogBtn := widget.NewButton("Clear", func() { + c.debugLog.SetText("Log cleared\n") }) - debugButtons := container.NewHBox(clearLogBtn, copyLogBtn) - rightPanel := container.NewVBox( securityPanel, debugPanel, - debugButtons, + clearLogBtn, ) connectionPanel := container.NewVBox( - widget.NewLabel("Server Address:"), + widget.NewLabel("Server:"), c.serverEntry, c.connectBtn, c.status, ) - chatArea := container.NewHSplit( - c.messages, - rightPanel, + userListScroll := container.NewScroll(c.userList) + userListScroll.SetMinSize(fyne.NewSize(200, 0)) + + userListPanel := container.NewBorder( + widget.NewCard("Online", "", widget.NewLabel("")), + nil, nil, nil, + userListScroll, ) - chatArea.SetOffset(0.65) + + leftSplit := container.NewHSplit(userListPanel, c.messages) + leftSplit.SetOffset(0.20) + + chatArea := container.NewHSplit(leftSplit, rightPanel) + chatArea.SetOffset(0.70) bottomBar := container.NewBorder( - nil, - nil, - widget.NewLabel("π¬ Input: "), + nil, nil, + widget.NewLabel("Input: "), widget.NewButton("Send", func() { c.sendMessage(c.input.Text) }), c.input, ) content := container.NewBorder( - connectionPanel, - bottomBar, - nil, - nil, + connectionPanel, + bottomBar, + nil, nil, chatArea, ) @@ -179,20 +382,37 @@ func (c *ChatClient) enableAutoReconnect() { for { time.Sleep(5 * time.Second) if !c.connected && c.autoReconnect && c.lastServer != "" { - // Wait at least 10 seconds between attempts if time.Since(lastAttempt) < 10*time.Second { continue } lastAttempt = time.Now() - - c.addDebugLog("π Auto-reconnecting to " + c.lastServer) - c.serverEntry.SetText(c.lastServer) - c.connectToServer() + + c.addDebugLog("Auto-reconnecting to " + c.lastServer) + fyne.Do(func() { + c.serverEntry.SetText(c.lastServer) + c.connectToServer() + }) } } }() } +func validateOnionAddress(addr string) error { + if addr == "" { + return fmt.Errorf("empty address") + } + + if !strings.Contains(addr, ".onion") { + return fmt.Errorf("only .onion addresses allowed") + } + + if !onionRegex.MatchString(addr) { + return fmt.Errorf("invalid .onion format") + } + + return nil +} + func (c *ChatClient) connectToServer() { if c.connected { c.disconnect() @@ -201,186 +421,164 @@ func (c *ChatClient) connectToServer() { serverAddr := c.serverEntry.Text if serverAddr == "" { - c.showError("Input Error", "Please enter server address") + c.showError("Error", "Enter .onion address") + return + } + + if err := validateOnionAddress(serverAddr); err != nil { + c.showError("Invalid Address", err.Error()) return } c.lastServer = serverAddr - c.debugLog.SetText("Starting new connection...\n") - c.addDebugLog(fmt.Sprintf("Target: %s", serverAddr)) + fyne.Do(func() { + c.debugLog.SetText("Connecting\n") + }) + c.addDebugLog("Target: " + serverAddr) - c.status.SetText("π‘ Connecting...") - c.connectBtn.Disable() + fyne.Do(func() { + c.status.SetText("Connecting...") + c.connectBtn.Disable() + }) - progress := dialog.NewCustom("Connecting", "Cancel", + progress := dialog.NewCustom("Connecting", "Cancel", widget.NewProgressBarInfinite(), c.window) progress.Show() go func() { - defer progress.Hide() - - isOnion := strings.HasSuffix(serverAddr, ".onion") || - strings.Contains(serverAddr, ".onion:") - - var conn net.Conn - var err error - - if isOnion { - c.status.SetText("π‘ Connecting through Tor...") - c.addDebugLog("π§
.onion address detected - using Tor") - conn, err = c.connectThroughTor(serverAddr) - } else { - c.status.SetText("π‘ Connecting directly...") - c.addDebugLog("π Regular address - direct connection") - conn, err = c.connectDirect(serverAddr) - } + defer fyne.Do(func() { progress.Hide() }) + conn, err := c.connectThroughTor(serverAddr) if err != nil { - c.showError("Connection Error", fmt.Sprintf("Error connecting to server: %v", err)) - c.addDebugLog(fmt.Sprintf("β Connection failed: %v", err)) - c.connectBtn.Enable() - c.status.SetText("π΄ Connection Failed") + c.showError("Connection Error", err.Error()) + c.addDebugLog(fmt.Sprintf("Connection failed: %v", err)) + fyne.Do(func() { + c.connectBtn.Enable() + c.status.SetText("Connection failed") + }) return } c.conn = conn - c.status.SetText("π‘ Establishing end-to-end encryption...") - c.addDebugLog("π Starting ECDH key exchange...") + fyne.Do(func() { + c.status.SetText("Encrypting...") + }) + c.addDebugLog("Starting ECDH") curve := ecdh.X25519() - privateKey, err := curve.GenerateKey(rand.Reader) - if err != nil { - c.showError("Crypto Error", fmt.Sprintf("Error generating private key: %v", err)) - c.addDebugLog(fmt.Sprintf("β Key generation failed: %v", err)) - c.disconnect() - return - } - privateKeyBuffer := memguard.NewBufferFromBytes(privateKey.Bytes()) - defer privateKeyBuffer.Destroy() - - publicKey := privateKey.PublicKey() - publicKeyBytes := publicKey.Bytes() - c.addDebugLog("π€ Sending public key...") - - c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) - totalSent := 0 - for totalSent < len(publicKeyBytes) { - n, err := c.conn.Write(publicKeyBytes[totalSent:]) + var encryptKey *ecdh.PrivateKey + if c.encryptKey != nil { + encryptKey = c.encryptKey + c.addDebugLog("Using persistent identity: " + c.fingerprint) + } else { + var err error + encryptKey, err = curve.GenerateKey(cryptorand.Reader) if err != nil { - c.showError("Connection Error", fmt.Sprintf("Error sending public key: %v", err)) - c.addDebugLog(fmt.Sprintf("β Failed to send public key: %v", err)) + c.showError("Crypto Error", err.Error()) c.disconnect() return } - totalSent += n + c.addDebugLog("Using temporary identity") + } + + encryptKeyBuffer := memguard.NewBufferFromBytes(encryptKey.Bytes()) + defer encryptKeyBuffer.Destroy() + + publicKey := encryptKey.PublicKey() + publicKeyBytes := publicKey.Bytes() + c.addDebugLog("Sending public key") + + c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) + if _, err := c.conn.Write(publicKeyBytes); err != nil { + c.showError("Connection Error", err.Error()) + c.disconnect() + return } c.conn.SetWriteDeadline(time.Time{}) - c.addDebugLog("β
Public key sent completely") - c.addDebugLog("π₯ Receiving server public key...") + c.addDebugLog("Receiving server key") serverPublicKeyBytes := make([]byte, 32) - _, err = io.ReadFull(conn, serverPublicKeyBytes) - if err != nil { - c.showError("Connection Error", fmt.Sprintf("Error receiving server public key: %v", err)) - c.addDebugLog(fmt.Sprintf("β Failed to receive server public key: %v", err)) + if _, err := io.ReadFull(conn, serverPublicKeyBytes); err != nil { + c.showError("Connection Error", err.Error()) c.disconnect() return } - c.addDebugLog("β
Received server public key") serverPublicKey, err := curve.NewPublicKey(serverPublicKeyBytes) if err != nil { - c.showError("Crypto Error", fmt.Sprintf("Error parsing server public key: %v", err)) - c.addDebugLog(fmt.Sprintf("β Invalid server public key: %v", err)) + c.showError("Crypto Error", err.Error()) c.disconnect() return } - c.addDebugLog("π’ Calculating shared secret...") - sharedSecret, err := privateKey.ECDH(serverPublicKey) + c.addDebugLog("Calculating shared secret") + + sharedSecret, err := encryptKey.ECDH(serverPublicKey) if err != nil { - c.showError("Crypto Error", fmt.Sprintf("Error calculating shared secret: %v", err)) - c.addDebugLog(fmt.Sprintf("β ECDH failed: %v", err)) + c.showError("Crypto Error", err.Error()) c.disconnect() return } - sharedSecretBuffer := memguard.NewBufferFromBytes(sharedSecret) - defer sharedSecretBuffer.Destroy() + c.addDebugLog("Starting HMAC auth") + if err := c.performMutualAuth(sharedSecret); err != nil { + c.showError("Auth Error", err.Error()) + c.disconnect() + return + } + c.addDebugLog("Auth successful") - c.addDebugLog("π Setting up AES-GCM encryption...") - block, err := aes.NewCipher(sharedSecret) + c.addDebugLog("Setting up AES-GCM") + block, err := aes.NewCipher(sharedSecret[:32]) if err != nil { - c.showError("Crypto Error", fmt.Sprintf("Error initializing AES cipher: %v", err)) - c.addDebugLog(fmt.Sprintf("β AES cipher creation failed: %v", err)) + c.showError("Crypto Error", err.Error()) c.disconnect() return } gcm, err := cipher.NewGCM(block) if err != nil { - c.showError("Crypto Error", fmt.Sprintf("Error initializing GCM mode: %v", err)) - c.addDebugLog(fmt.Sprintf("β GCM cipher creation failed: %v", err)) + c.showError("Crypto Error", err.Error()) c.disconnect() return } + sharedSecretBuffer := memguard.NewBufferFromBytes(sharedSecret) + c.sharedSecret = sharedSecretBuffer.Seal() + sharedSecretBuffer.Destroy() + c.gcm = gcm c.connected = true - c.input.Enable() - c.connectBtn.SetText("πͺ Disconnect") - c.connectBtn.OnTapped = c.disconnect - c.connectBtn.Enable() - - if isOnion { - c.status.SetText("π’ Connected via Tor - Maximum Anonymity") - c.addMessage("System", "β
Connected through Tor") - c.addMessage("System", "π§
Anonymous connection established") - c.addDebugLog("π Tor connection fully established!") - } else { - c.status.SetText("π’ Connected - Maximum Security Active") - c.addMessage("System", "β
Connected to secure server") - c.addDebugLog("π Direct connection fully established!") - } - - c.addMessage("System", "π End-to-end encryption established") - c.addMessage("System", "π¬ You can now send secure messages") - c.addDebugLog("β
Ready for secure messaging") - // Start receiving messages + fyne.Do(func() { + c.input.Enable() + c.connectBtn.SetText("Disconnect") + c.connectBtn.OnTapped = c.disconnect + c.connectBtn.Enable() + c.status.SetText("Connected (E2E)") + }) + + c.addMessage("System", "Connected via Tor") + c.addMessage("System", "E2E encryption active") + c.addDebugLog("Session established") + go c.receiveMessages() - - // Start heartbeat after everything is ready - c.startHeartbeat() }() } func (c *ChatClient) connectThroughTor(serverAddr string) (net.Conn, error) { - c.addDebugLog("π§
Starting Tor connection...") - c.addDebugLog("π§ͺ Testing Tor circuit...") - - // Test if Tor SOCKS proxy is running - testConn, testErr := net.DialTimeout("tcp", "127.0.0.1:9050", 2*time.Second) - if testErr == nil { - testConn.Close() - c.addDebugLog("β
Tor SOCKS proxy is responsive") - } else { - c.addDebugLog(fmt.Sprintf("β οΈ Warning: Tor proxy test failed: %v", testErr)) - } - + c.addDebugLog("Starting Tor connection") + var conn net.Conn proxyURLs := []string{ "socks5://127.0.0.1:9050", "socks5://localhost:9050", } - - for i, proxyURL := range proxyURLs { - c.addDebugLog(fmt.Sprintf("π Attempt %d: Using proxy %s", i+1, proxyURL)) - + + for _, proxyURL := range proxyURLs { torProxyUrl, _ := url.Parse(proxyURL) if torProxyUrl == nil { - c.addDebugLog("β Invalid proxy URL") continue } @@ -391,138 +589,79 @@ func (c *ChatClient) connectThroughTor(serverAddr string) (net.Conn, error) { dialer, dialErr := proxy.FromURL(torProxyUrl, baseDialer) if dialErr != nil { - c.addDebugLog(fmt.Sprintf("β Error creating dialer: %v", dialErr)) continue } - c.addDebugLog(fmt.Sprintf("π Connecting to %s through Tor...", serverAddr)) - + c.addDebugLog("Connecting via " + proxyURL) + var connErr error conn, connErr = dialer.Dial("tcp", serverAddr) if connErr != nil { - c.addDebugLog(fmt.Sprintf("β Attempt %d failed: %v", i+1, connErr)) - if i < len(proxyURLs)-1 { - time.Sleep(2 * time.Second) - continue - } + c.addDebugLog("Failed: " + connErr.Error()) + continue } else { - c.addDebugLog(fmt.Sprintf("β
Connection successful with proxy %s", proxyURL)) + c.addDebugLog("Connected via " + proxyURL) break } } - + if conn == nil { - return nil, fmt.Errorf("Tor SOCKS connection failed after all attempts") + return nil, fmt.Errorf("Tor connection failed") } - c.addDebugLog("β
TCP connection established through Tor") + c.addDebugLog("TCP established") + return conn, nil +} - tlsConfig := &tls.Config{ - InsecureSkipVerify: true, - ServerName: "", - MinVersion: tls.VersionTLS12, - MaxVersion: tls.VersionTLS13, - CipherSuites: []uint16{ - tls.TLS_AES_256_GCM_SHA384, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - }, - CurvePreferences: []tls.CurveID{ - tls.X25519, - tls.CurveP256, - }, - } +func (c *ChatClient) performMutualAuth(sharedSecret []byte) error { + c.conn.SetDeadline(time.Now().Add(30 * time.Second)) + defer c.conn.SetDeadline(time.Time{}) - c.addDebugLog("π Starting TLS handshake...") - - tlsConn := tls.Client(conn, tlsConfig) - tlsConn.SetDeadline(time.Now().Add(45 * time.Second)) - - if err := tlsConn.Handshake(); err != nil { - conn.Close() - c.addDebugLog(fmt.Sprintf("β TLS handshake failed: %v", err)) - return nil, fmt.Errorf("TLS handshake through Tor failed: %v", err) + serverChallenge := make([]byte, 32) + if _, err := io.ReadFull(c.conn, serverChallenge); err != nil { + return fmt.Errorf("receive challenge failed: %v", err) } - - tlsConn.SetDeadline(time.Time{}) - - c.addDebugLog("β
TLS connection established") - return tlsConn, nil -} + h := hmac.New(sha256.New, sharedSecret) + h.Write(serverChallenge) + clientResponse := h.Sum(nil) -func (c *ChatClient) connectDirect(serverAddr string) (net.Conn, error) { - c.addDebugLog("π Starting direct connection...") - - tlsConfig := &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_AES_256_GCM_SHA384, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - }, - CurvePreferences: []tls.CurveID{ - tls.X25519, - tls.CurveP256, - tls.CurveP384, - }, + clientChallenge := make([]byte, 32) + if _, err := cryptorand.Read(clientChallenge); err != nil { + return fmt.Errorf("challenge generation failed: %v", err) } - c.addDebugLog(fmt.Sprintf("π‘ Connecting to %s...", serverAddr)) - - conn, err := net.DialTimeout("tcp", serverAddr, 30*time.Second) - if err != nil { - c.addDebugLog(fmt.Sprintf("β TCP connection failed: %v", err)) - return nil, fmt.Errorf("direct TCP connection failed: %v", err) + if _, err := c.conn.Write(clientResponse); err != nil { + return fmt.Errorf("send response failed: %v", err) + } + if _, err := c.conn.Write(clientChallenge); err != nil { + return fmt.Errorf("send challenge failed: %v", err) } - c.addDebugLog("β
TCP connection established") - c.addDebugLog("π Starting TLS handshake...") - - tlsConn := tls.Client(conn, tlsConfig) - if err := tlsConn.Handshake(); err != nil { - conn.Close() - c.addDebugLog(fmt.Sprintf("β TLS handshake failed: %v", err)) - return nil, fmt.Errorf("TLS handshake failed: %v", err) + serverResponse := make([]byte, 32) + if _, err := io.ReadFull(c.conn, serverResponse); err != nil { + return fmt.Errorf("receive response failed: %v", err) } - c.addDebugLog("β
TLS connection established") + h.Reset() + h.Write(clientChallenge) + expectedServerMAC := h.Sum(nil) + + if !hmac.Equal(serverResponse, expectedServerMAC) { + return fmt.Errorf("server authentication failed") + } - return tlsConn, nil + return nil } func (c *ChatClient) addDebugLog(message string) { timestamp := time.Now().Format("15:04:05") logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message) - - currentText := c.debugLog.Text - c.debugLog.SetText(currentText + logEntry) - - c.debugLog.CursorRow = len(strings.Split(c.debugLog.Text, "\n")) - 1 -} -func (c *ChatClient) startHeartbeat() { - go func() { - for { - time.Sleep(30 * time.Second) - if !c.connected { - c.addDebugLog("π Heartbeat stopped - not connected") - return - } - - c.addDebugLog("π Sending keepalive ping...") - if err := c.sendEncryptedMessage("/ping"); err != nil { - c.addDebugLog(fmt.Sprintf("β Keepalive failed: %v", err)) - if c.connected { - c.addMessage("System", "β Connection lost - heartbeat failed") - c.disconnect() - } - return - } - } - }() + fyne.Do(func() { + c.debugLog.SetText(c.debugLog.Text + logEntry) + c.debugLog.CursorRow = len(strings.Split(c.debugLog.Text, "\n")) - 1 + }) } func (c *ChatClient) disconnect() { @@ -531,22 +670,77 @@ func (c *ChatClient) disconnect() { } c.connected = false - c.addDebugLog("π Initiating disconnect...") + c.addDebugLog("Disconnecting") if c.conn != nil { c.sendEncryptedMessage("/quit") time.Sleep(100 * time.Millisecond) c.conn.Close() + c.conn = nil } - - c.input.Disable() - c.connectBtn.SetText("π Connect to Secure Server") - c.connectBtn.OnTapped = c.connectToServer - c.connectBtn.Enable() - c.status.SetText("π΄ Disconnected - Ready to connect") - - c.addMessage("System", "π΄ Disconnected from server") - c.addDebugLog("β
Disconnection complete - session cleaned") + + if c.sharedSecret != nil { + buf, _ := c.sharedSecret.Open() + if buf != nil { + buf.Destroy() + } + c.sharedSecret = nil + } + c.gcm = nil + + fyne.Do(func() { + c.input.Disable() + c.connectBtn.SetText("Connect") + c.connectBtn.OnTapped = c.connectToServer + c.connectBtn.Enable() + c.status.SetText("Disconnected") + }) + + c.addMessage("System", "Disconnected") + c.addDebugLog("Keys wiped") +} + +func (c *ChatClient) panic() { + c.addDebugLog("PANIC MODE") + + if c.conn != nil { + c.conn.Close() + c.conn = nil + } + + if c.sharedSecret != nil { + buf, _ := c.sharedSecret.Open() + if buf != nil { + buf.Destroy() + } + c.sharedSecret = nil + } + + if c.signKey != nil { + c.signKey = nil + } + + if c.encryptKey != nil { + c.encryptKey = nil + } + + c.gcm = nil + c.connected = false + c.fingerprint = "" + c.username = "" + + fyne.Do(func() { + c.input.Disable() + c.connectBtn.SetText("Connect") + c.connectBtn.OnTapped = c.connectToServer + c.connectBtn.Enable() + c.status.SetText("PANIC - All keys destroyed") + }) + + c.addMessage("System", "PANIC: Keys destroyed") + c.addDebugLog("Memory wiped") + + memguard.Purge() } func (c *ChatClient) sendMessage(message string) { @@ -554,153 +748,199 @@ func (c *ChatClient) sendMessage(message string) { return } - c.addDebugLog(fmt.Sprintf("π€ Sending message: %s", message)) + c.addDebugLog("Sending: " + message) err := c.sendEncryptedMessage(message) if err != nil { - c.addDebugLog(fmt.Sprintf("β Send error: %v", err)) - c.showError("Send Error", fmt.Sprintf("Error sending message: %v", err)) + c.addDebugLog("Send error: " + err.Error()) + c.showError("Send Error", err.Error()) return } - c.addDebugLog("β
Message sent successfully") c.addMessage("You", message) - c.input.SetText("") + fyne.Do(func() { + c.input.SetText("") + }) } -func (c *ChatClient) sendEncryptedMessage(message string) error { - if c.gcm == nil { - return fmt.Errorf("encryption not initialized") +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("data too short") } - - if c.conn == nil { - return fmt.Errorf("connection not established") + + originalLen := binary.BigEndian.Uint16(paddedData[0:2]) + + if int(originalLen) > len(paddedData)-2 { + return nil, fmt.Errorf("invalid padding") } + return paddedData[2 : 2+originalLen], nil +} + +func randomDelay() { + delay := minRandomDelay + rand.Intn(maxRandomDelay-minRandomDelay) + time.Sleep(time.Duration(delay) * time.Millisecond) +} + +func (c *ChatClient) sendEncryptedMessage(message string) error { + if c.gcm == nil || c.conn == nil { + return fmt.Errorf("not connected") + } + + randomDelay() + + paddedMessage := padMessage([]byte(message)) + nonce := make([]byte, 12) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return fmt.Errorf("failed to generate nonce: %v", err) + if _, err := io.ReadFull(cryptorand.Reader, nonce); err != nil { + return fmt.Errorf("nonce generation failed: %v", err) } - encrypted := c.gcm.Seal(nil, nonce, []byte(message), nil) + encrypted := c.gcm.Seal(nil, nonce, paddedMessage, nil) data := append(nonce, encrypted...) - - _, err := c.conn.Write(data) - if err != nil { - return fmt.Errorf("failed to write message: %v", err) - } - return nil + _, err := c.conn.Write(data) + return err } func (c *ChatClient) receiveMessages() { buf := make([]byte, 8192) - - c.addDebugLog("π‘ Starting message receive loop") - + + c.addDebugLog("Starting receive loop") + for c.connected { - // NO deadline for persistent connections - c.conn.SetReadDeadline(time.Time{}) - + c.conn.SetReadDeadline(time.Time{}) + n, err := c.conn.Read(buf) if err != nil { if err == io.EOF { - c.addDebugLog("π΄ Server closed connection (EOF)") + c.addDebugLog("Server closed connection") } else { - c.addDebugLog(fmt.Sprintf("β Read error: %v", err)) + c.addDebugLog("Read error: " + err.Error()) } if c.connected { - c.addMessage("System", "β Connection lost") + c.addMessage("System", "Connection lost") c.disconnect() } return } - + if n == 0 { - c.addDebugLog("β οΈ Read 0 bytes, continuing...") continue } - - c.addDebugLog(fmt.Sprintf("π₯ Received %d bytes", n)) - - // Decrypt the message + + c.addDebugLog(fmt.Sprintf("Received %d bytes", n)) + message, err := c.decryptMessage(buf[:n]) if err != nil { - c.addDebugLog(fmt.Sprintf("β Decrypt error: %v", err)) + c.addDebugLog("Decrypt error: " + err.Error()) continue } - - c.addDebugLog(fmt.Sprintf("π Decrypted: %s", message)) - - // Handle special commands - if message == "/pong" { - c.addDebugLog("π Pong received from server") - continue // Don't show pong in messages - } - - // Try to parse as JSON - var msg struct { - From string `json:"from"` - Content string `json:"content"` - Type string `json:"type"` - Time string `json:"time"` + + var userListMsg UserListMessage + if err := json.Unmarshal([]byte(message), &userListMsg); err == nil && userListMsg.Type == "user_list" { + c.onlineUsers = userListMsg.Users + fyne.Do(func() { + c.userList.Refresh() + }) + c.addMessage("System", fmt.Sprintf("Online: %d users", len(userListMsg.Users))) + continue } - + + var msg Message if err := json.Unmarshal([]byte(message), &msg); err != nil { - // Not JSON, show as plain message - c.addDebugLog(fmt.Sprintf("π Plain message: %s", message)) c.addMessage("Server", message) } else { - c.addDebugLog(fmt.Sprintf("π JSON from %s: %s", msg.From, msg.Content)) - if msg.Type == "system" { + switch msg.Type { + case "system": c.addMessage("System", msg.Content) - } else { + case "private": + c.addMessage("PM-"+msg.From, msg.Content) + case "error": + c.addMessage("System", "Error: "+msg.Content) + default: c.addMessage(msg.From, msg.Content) } } } - - c.addDebugLog("π‘ Receive loop ended") + + c.addDebugLog("Receive loop ended") } func (c *ChatClient) decryptMessage(data []byte) (string, error) { if len(data) < 12 { - return "", fmt.Errorf("message too short: %d bytes", len(data)) + return "", fmt.Errorf("message too short") } nonce := data[:12] ciphertext := data[12:] - plaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil) + paddedPlaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", fmt.Errorf("decryption failed: %v", err) } + plaintext, err := unpadMessage(paddedPlaintext) + if err != nil { + return "", fmt.Errorf("unpad failed: %v", err) + } + return string(plaintext), nil } +func (c *ChatClient) onUserClick(username string) { + fyne.Do(func() { + c.input.SetText("/pm " + username + " ") + c.input.CursorColumn = len(c.input.Text) + c.window.Canvas().Focus(c.input) + }) + c.addDebugLog("Ready to PM " + username) +} + func (c *ChatClient) addMessage(sender, message string) { timestamp := time.Now().Format("15:04:05") formatted := fmt.Sprintf("[%s] %s: %s", timestamp, sender, message) - + c.messageList = append(c.messageList, formatted) - c.messages.Refresh() - c.messages.ScrollToBottom() + fyne.Do(func() { + c.messages.Refresh() + c.messages.ScrollToBottom() + }) } func (c *ChatClient) showError(title, message string) { - dialog.ShowError(fmt.Errorf(message), c.window) - c.addMessage("System", "β "+title+": "+message) - c.addDebugLog("β " + title + ": " + message) + fyne.Do(func() { + dialog.ShowError(fmt.Errorf(message), c.window) + }) + c.addMessage("System", title+": "+message) + c.addDebugLog(title + ": " + message) } func init() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - + go func() { <-sigChan - fmt.Printf("\nπ NoshiTalk Client v%s shutting down...\n", version) + fmt.Printf("\nShutting down\n") memguard.Purge() os.Exit(0) }() |
