summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2025-10-06 10:25:04 +0200
committerGitHub <noreply@github.com>2025-10-06 10:25:04 +0200
commit788d5f759d6136c1491ac64bb25b5f53fe65c4df (patch)
tree312ff9b8e4aaefddc482251a8cb9f284f3b0a03d
parent74f06db22b9ec8cb1591520cd7256ce15d9c4e1e (diff)
downloadnoshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.tar.gz
noshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.tar.xz
noshitalk-788d5f759d6136c1491ac64bb25b5f53fe65c4df.zip
Update noshitalk-client.go
-rw-r--r--noshitalk-client.go956
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)
}()