summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2025-11-25 20:20:52 +0100
committerGitHub <noreply@github.com>2025-11-25 20:20:52 +0100
commitc9f4d26216327def79cf2d48152dda14c778112b (patch)
treef6a3e3f4b87c585aba13b7f948291449cf589431
parent9b4caf3e5ffad219ea55ea6db7ad19940c5f83d9 (diff)
downloadnoshitalk-c9f4d26216327def79cf2d48152dda14c778112b.tar.gz
noshitalk-c9f4d26216327def79cf2d48152dda14c778112b.tar.xz
noshitalk-c9f4d26216327def79cf2d48152dda14c778112b.zip
Delete noshitalk-client.go
-rw-r--r--noshitalk-client.go947
1 files changed, 0 insertions, 947 deletions
diff --git a/noshitalk-client.go b/noshitalk-client.go
deleted file mode 100644
index bed425c..0000000
--- a/noshitalk-client.go
+++ /dev/null
@@ -1,947 +0,0 @@
-package main
-
-import (
- "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"
- "syscall"
- "time"
-
- "github.com/awnumar/memguard"
- "golang.org/x/net/proxy"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/app"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/dialog"
- "fyne.io/fyne/v2/widget"
- "fyne.io/fyne/v2/theme"
-)
-
-type ChatClient struct {
- 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
- userList *widget.List
- onlineUsers []string
- username string
- fingerprint string
- signKey *ecdh.PrivateKey
- encryptKey *ecdh.PrivateKey
-}
-
-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-gui")
- client := &ChatClient{
- 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()
- client.enableAutoReconnect()
- 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 GUI v%s", version))
- c.window.SetIcon(theme.ComputerIcon())
- c.window.Resize(fyne.NewSize(1200, 800))
-
- c.status = widget.NewLabel("Disconnected")
- c.status.TextStyle.Bold = true
-
- c.serverEntry = widget.NewEntry()
- c.serverEntry.SetText("")
- c.serverEntry.SetPlaceHolder("abc...xyz.onion:8080")
-
- c.connectBtn = widget.NewButton("Connect", c.connectToServer)
- c.connectBtn.Importance = widget.HighImportance
-
- c.messages = widget.NewList(
- func() int {
- return len(c.messageList)
- },
- func() fyne.CanvasObject {
- 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("Message...")
- c.input.Disable()
- c.input.OnSubmitted = c.sendMessage
-
- autoReconnectCheck := widget.NewCheck("Auto-reconnect", func(checked bool) {
- c.autoReconnect = checked
- })
-
- 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("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\n")
-
- autoReconnectCheck.SetChecked(true)
-
- debugScroll := container.NewScroll(c.debugLog)
- debugScroll.SetMinSize(fyne.NewSize(350, 300))
-
- debugPanel := widget.NewCard("Debug", "", debugScroll)
-
- clearLogBtn := widget.NewButton("Clear", func() {
- c.debugLog.SetText("Log cleared\n")
- })
-
- rightPanel := container.NewVBox(
- securityPanel,
- debugPanel,
- clearLogBtn,
- )
-
- connectionPanel := container.NewVBox(
- widget.NewLabel("Server:"),
- c.serverEntry,
- c.connectBtn,
- c.status,
- )
-
- userListScroll := container.NewScroll(c.userList)
- userListScroll.SetMinSize(fyne.NewSize(200, 0))
-
- userListPanel := container.NewBorder(
- widget.NewCard("Online", "", widget.NewLabel("")),
- nil, nil, nil,
- userListScroll,
- )
-
- 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: "),
- widget.NewButton("Send", func() { c.sendMessage(c.input.Text) }),
- c.input,
- )
-
- content := container.NewBorder(
- connectionPanel,
- bottomBar,
- nil, nil,
- chatArea,
- )
-
- c.window.SetContent(content)
-}
-
-func (c *ChatClient) enableAutoReconnect() {
- go func() {
- lastAttempt := time.Now()
- for {
- time.Sleep(5 * time.Second)
- if !c.connected && c.autoReconnect && c.lastServer != "" {
- if time.Since(lastAttempt) < 10*time.Second {
- continue
- }
- lastAttempt = time.Now()
-
- 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()
- return
- }
-
- serverAddr := c.serverEntry.Text
- if serverAddr == "" {
- c.showError("Error", "Enter .onion address")
- return
- }
-
- if err := validateOnionAddress(serverAddr); err != nil {
- c.showError("Invalid Address", err.Error())
- return
- }
-
- c.lastServer = serverAddr
- fyne.Do(func() {
- c.debugLog.SetText("Connecting\n")
- })
- c.addDebugLog("Target: " + serverAddr)
-
- fyne.Do(func() {
- c.status.SetText("Connecting...")
- c.connectBtn.Disable()
- })
-
- progress := dialog.NewCustom("Connecting", "Cancel",
- widget.NewProgressBarInfinite(), c.window)
- progress.Show()
-
- go func() {
- defer fyne.Do(func() { progress.Hide() })
-
- conn, err := c.connectThroughTor(serverAddr)
- if err != nil {
- 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
- fyne.Do(func() {
- c.status.SetText("Encrypting...")
- })
- c.addDebugLog("Starting ECDH")
-
- curve := ecdh.X25519()
-
- 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("Crypto Error", err.Error())
- c.disconnect()
- return
- }
- 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("Receiving server key")
- serverPublicKeyBytes := make([]byte, 32)
- if _, err := io.ReadFull(conn, serverPublicKeyBytes); err != nil {
- c.showError("Connection Error", err.Error())
- c.disconnect()
- return
- }
-
- serverPublicKey, err := curve.NewPublicKey(serverPublicKeyBytes)
- if err != nil {
- c.showError("Crypto Error", err.Error())
- c.disconnect()
- return
- }
-
- c.addDebugLog("Calculating shared secret")
-
- sharedSecret, err := encryptKey.ECDH(serverPublicKey)
- if err != nil {
- c.showError("Crypto Error", err.Error())
- c.disconnect()
- return
- }
-
- 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")
- block, err := aes.NewCipher(sharedSecret[:32])
- if err != nil {
- c.showError("Crypto Error", err.Error())
- c.disconnect()
- return
- }
-
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- 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
-
- 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()
- }()
-}
-
-func (c *ChatClient) connectThroughTor(serverAddr string) (net.Conn, error) {
- c.addDebugLog("Starting Tor connection")
-
- var conn net.Conn
- proxyURLs := []string{
- "socks5://127.0.0.1:9050",
- "socks5://localhost:9050",
- }
-
- for _, proxyURL := range proxyURLs {
- torProxyUrl, _ := url.Parse(proxyURL)
- if torProxyUrl == nil {
- continue
- }
-
- baseDialer := &net.Dialer{
- Timeout: 90 * time.Second,
- KeepAlive: 30 * time.Second,
- }
-
- dialer, dialErr := proxy.FromURL(torProxyUrl, baseDialer)
- if dialErr != nil {
- continue
- }
-
- c.addDebugLog("Connecting via " + proxyURL)
-
- var connErr error
- conn, connErr = dialer.Dial("tcp", serverAddr)
- if connErr != nil {
- c.addDebugLog("Failed: " + connErr.Error())
- continue
- } else {
- c.addDebugLog("Connected via " + proxyURL)
- break
- }
- }
-
- if conn == nil {
- return nil, fmt.Errorf("Tor connection failed")
- }
-
- c.addDebugLog("TCP established")
- return conn, nil
-}
-
-func (c *ChatClient) performMutualAuth(sharedSecret []byte) error {
- c.conn.SetDeadline(time.Now().Add(30 * time.Second))
- defer c.conn.SetDeadline(time.Time{})
-
- serverChallenge := make([]byte, 32)
- if _, err := io.ReadFull(c.conn, serverChallenge); err != nil {
- return fmt.Errorf("receive challenge failed: %v", err)
- }
-
- h := hmac.New(sha256.New, sharedSecret)
- h.Write(serverChallenge)
- clientResponse := h.Sum(nil)
-
- clientChallenge := make([]byte, 32)
- if _, err := cryptorand.Read(clientChallenge); err != nil {
- return fmt.Errorf("challenge generation 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)
- }
-
- serverResponse := make([]byte, 32)
- if _, err := io.ReadFull(c.conn, serverResponse); err != nil {
- return fmt.Errorf("receive response failed: %v", err)
- }
-
- h.Reset()
- h.Write(clientChallenge)
- expectedServerMAC := h.Sum(nil)
-
- if !hmac.Equal(serverResponse, expectedServerMAC) {
- return fmt.Errorf("server authentication failed")
- }
-
- return nil
-}
-
-func (c *ChatClient) addDebugLog(message string) {
- timestamp := time.Now().Format("15:04:05")
- logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
-
- 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() {
- if !c.connected {
- return
- }
-
- c.connected = false
- c.addDebugLog("Disconnecting")
-
- if c.conn != nil {
- c.sendEncryptedMessage("/quit")
- time.Sleep(100 * time.Millisecond)
- c.conn.Close()
- c.conn = nil
- }
-
- 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) {
- if !c.connected || message == "" {
- return
- }
-
- c.addDebugLog("Sending: " + message)
-
- err := c.sendEncryptedMessage(message)
- if err != nil {
- c.addDebugLog("Send error: " + err.Error())
- c.showError("Send Error", err.Error())
- return
- }
-
- c.addMessage("You", message)
- fyne.Do(func() {
- c.input.SetText("")
- })
-}
-
-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")
- }
-
- 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(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 *ChatClient) receiveMessages() {
- buf := make([]byte, 8192)
-
- c.addDebugLog("Starting receive loop")
-
- for c.connected {
- c.conn.SetReadDeadline(time.Time{})
-
- n, err := c.conn.Read(buf)
- if err != nil {
- if err == io.EOF {
- c.addDebugLog("Server closed connection")
- } else {
- c.addDebugLog("Read error: " + err.Error())
- }
- if c.connected {
- c.addMessage("System", "Connection lost")
- c.disconnect()
- }
- return
- }
-
- if n == 0 {
- continue
- }
-
- c.addDebugLog(fmt.Sprintf("Received %d bytes", n))
-
- message, err := c.decryptMessage(buf[:n])
- if err != nil {
- c.addDebugLog("Decrypt error: " + err.Error())
- continue
- }
-
- 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 {
- c.addMessage("Server", message)
- } else {
- switch msg.Type {
- case "system":
- c.addMessage("System", msg.Content)
- 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")
-}
-
-func (c *ChatClient) decryptMessage(data []byte) (string, error) {
- if len(data) < 12 {
- return "", fmt.Errorf("message too short")
- }
-
- nonce := data[:12]
- ciphertext := data[12:]
-
- 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)
- fyne.Do(func() {
- c.messages.Refresh()
- c.messages.ScrollToBottom()
- })
-}
-
-func (c *ChatClient) showError(title, message string) {
- 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("\nShutting down\n")
- memguard.Purge()
- os.Exit(0)
- }()
-}