From 8b0ba9b3f902ef93942fec1c9e5520929516b00f Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Fri, 25 Feb 2022 06:19:12 -0800 Subject: [PATCH] Begin refactor --- .gitignore | 1 + README.md | 1 + chat/message.go | 50 ++ config/config.go | 40 ++ connection/command.go | 27 + connection/command_nick.go | 20 + connection/command_privmsg.go | 58 ++ {e2eirc => connection}/connection.go | 52 +- {e2eirc => connection}/control_commands.go | 10 +- connection/keys.go | 132 ++++ connection/session.go | 23 + {e2eirc => crypto}/aes.go | 18 +- crypto/rsa.go | 79 ++ db/storage.go | 38 + {e2eirc => db}/trust_registry.go | 29 +- e2eirc/banner.go | 38 - e2eirc/command.go | 25 - e2eirc/command_nick.go | 20 - e2eirc/command_privmsg.go | 58 -- e2eirc/config.go | 29 - e2eirc/message.go | 48 -- e2eirc/rsa.go | 206 ------ e2eirc/server.go | 17 - e2eirc/storage.go | 32 - go.mod | 27 + go.sum | 797 +++++++++++++++++++++ main.go | 11 - {e2eirc => peer}/peer.go | 93 +-- {e2eirc => peer}/trust.go | 28 +- readme.md | 86 --- server.go | 17 + e2eirc/tcp.go => tcp.go | 0 32 files changed, 1431 insertions(+), 679 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 chat/message.go create mode 100644 config/config.go create mode 100644 connection/command.go create mode 100644 connection/command_nick.go create mode 100644 connection/command_privmsg.go rename {e2eirc => connection}/connection.go (57%) rename {e2eirc => connection}/control_commands.go (83%) create mode 100644 connection/keys.go create mode 100644 connection/session.go rename {e2eirc => crypto}/aes.go (84%) create mode 100644 crypto/rsa.go create mode 100644 db/storage.go rename {e2eirc => db}/trust_registry.go (62%) delete mode 100644 e2eirc/banner.go delete mode 100644 e2eirc/command.go delete mode 100644 e2eirc/command_nick.go delete mode 100644 e2eirc/command_privmsg.go delete mode 100644 e2eirc/config.go delete mode 100644 e2eirc/message.go delete mode 100644 e2eirc/rsa.go delete mode 100644 e2eirc/server.go delete mode 100644 e2eirc/storage.go create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 main.go rename {e2eirc => peer}/peer.go (54%) rename {e2eirc => peer}/trust.go (85%) delete mode 100644 readme.md create mode 100644 server.go rename e2eirc/tcp.go => tcp.go (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d713ae --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +## Total overhaul of [e2eirc](https://github.com/novus0rdo/e2eirc/tree/master/e2eirc). diff --git a/chat/message.go b/chat/message.go new file mode 100644 index 0000000..8738107 --- /dev/null +++ b/chat/message.go @@ -0,0 +1,50 @@ +package chat + +import ( + "strings" + + "git.tcp.direct/tcp.direct/IR5EC/connection" +) + +type Msg struct { + buf []byte + Conn *connection.Session + + Components []string + SenderComponents []string + Offset int +} + +func messageWithBytesAndConnection(buf []byte, conn *connection.Session) Msg { + return Msg{buf: buf, Conn: conn} +} + +func (m *Msg) encrypt() { + str := string(m.buf) + m.Components = strings.Split(str, " ") + + commandName := m.Components[0] + command := connection.Commands[commandName] + if command != nil { + command.encrypt(m) + return + } + + m.Conn.writeServer(m.buf) +} + +func (m *Msg) decrypt() { + str := string(m.buf) + + m.Components = strings.Split(str, " ") + m.Offset = 1 + + commandName := m.Components[1] + command := connection.Commands[commandName] + if command != nil { + command.decrypt(m) + return + } + + m.Conn.writeClient(m.buf) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..8b73ff7 --- /dev/null +++ b/config/config.go @@ -0,0 +1,40 @@ +package config + +import ( + "flag" + "os" +) + +type Config struct { + RemotePort int + RemoteHost string + + LocalPort int + LocalHost string + + StorePassword string + DataPath string +} + +const Title = "IR5EC" + +var Opt = Config{} + +func init() { + if h, err := os.UserHomeDir(); err == nil { + Opt.DataPath = h + "/.config/" + Title + } +} + +func ParseFlags() { + flag.IntVar(&Opt.RemotePort, "port", 6667, "The port of the remote IRC server") + flag.StringVar(&Opt.RemoteHost, "host", "0.0.0.0", "The host of the remote IRC server") + + flag.IntVar(&Opt.LocalPort, "local_port", 6666, "The local IRC port to connect to") + flag.StringVar(&Opt.LocalHost, "local_host", "0.0.0.0", "The address to listen on") + + flag.StringVar(&Opt.StorePassword, "key", "", "The password to decrypt your private key, leave blank for STDIN") + // flag.StringVar(&Settings.DataPath, "dir", "", "The path to the directory containing your private and trusted keys, leave blank for ~/.e2eirc") + + flag.Parse() +} diff --git a/connection/command.go b/connection/command.go new file mode 100644 index 0000000..0a67ebb --- /dev/null +++ b/connection/command.go @@ -0,0 +1,27 @@ +package connection + +import "git.tcp.direct/tcp.direct/IR5EC/chat" + +type cryptoFunc func(m *chat.Msg) + +type Command struct { + Name string + Enc cryptoFunc + Dec cryptoFunc +} + +var Commands = map[string]*Command{} + +func newCommand(name string) *Command { + c := Command{Name: name} + return &c +} + +func (c *Command) register() { + Commands[c.Name] = c +} + +func RegisterCommands() { + commandPrivMSG().register() + commandNICK().register() +} diff --git a/connection/command_nick.go b/connection/command_nick.go new file mode 100644 index 0000000..291b08d --- /dev/null +++ b/connection/command_nick.go @@ -0,0 +1,20 @@ +package connection + +func commandNICK() *Command { + c := newCommand("NICK") + c.Enc = encryptNICK + c.Dec = decryptNICK + + return c +} + +func encryptNICK(m *Msg) { + name := m.Components[m.offset+1] + m.Conn.name = name[:len(name)-2] + + m.Conn.writeServer(m.buf) +} + +func decryptNICK(m *Msg) { + m.Conn.writeClient(m.buf) +} diff --git a/connection/command_privmsg.go b/connection/command_privmsg.go new file mode 100644 index 0000000..bf223d3 --- /dev/null +++ b/connection/command_privmsg.go @@ -0,0 +1,58 @@ +package connection + +import ( + "fmt" + "strings" +) + +func commandPrivMSG() *Command { + c := newCommand("PRIVMSG") + c.Enc = encryptPrivMSG + c.Dec = decryptPrivMSG + + return c +} + +func encryptPrivMSG(m *Msg) { + msgIndex := m.offset + 2 + + msg := strings.Join(m.Components[msgIndex:], " ") + + start := 0 + if msg[0] == ':' { + start = 1 + } + + msg = msg[start : len(msg)-2] + + if m.Components[m.offset+1] == trustUser { + m.Conn.handleTrustMessage(msg) + return + } + + trimmedComponents := m.Components[:msgIndex] + + msg = m.Conn.aesEncryptString(msg) + + result := strings.Join(trimmedComponents, " ") + " :" + msg + "\n" + fmt.Print("<<<", result) + + m.Conn.writeServer([]byte(result)) +} + +func decryptPrivMSG(m *Msg) { + // Extract Sender + sender := m.Components[0][1:] + m.senderComponents = strings.SplitN(sender, "!~", 2) + + sender = m.senderComponents[0] + + // If Msg is coming from server and has trust user's username + // just block it + if sender == trustUser { + return + } + + peer := m.Conn.peerWithID(sender) + peer.decryptPrivMSG(m) +} diff --git a/e2eirc/connection.go b/connection/connection.go similarity index 57% rename from e2eirc/connection.go rename to connection/connection.go index 2ee41f6..03a72c2 100644 --- a/e2eirc/connection.go +++ b/connection/connection.go @@ -1,4 +1,4 @@ -package e2eirc +package connection import ( "crypto/rsa" @@ -6,17 +6,21 @@ import ( "io" "net" "strconv" + + "github.com/awnumar/memguard" + + "git.tcp.direct/tcp.direct/IR5EC/peer" ) -type connection struct { +type Session struct { client *net.Conn server *net.Conn name string - peerRegistry map[string]*peer + peerRegistry map[string]*peer.Peer - encryptionKey []byte + encryptionKey *memguard.Enclave trustListening bool trustQueue chan *trustRequest @@ -26,14 +30,28 @@ type connection struct { privateKey *rsa.PrivateKey } +func (c *Session) SetEncryptionKey(b []byte) { + c.encryptionKey = memguard.NewEnclave(b) +} + +func (c *Session) EncryptionKey() []byte { + o, err := c.encryptionKey.Open() + if err != nil { + return nil + } + o.RLock() + defer o.RUnlock() + return o.Bytes() +} + const bufferSize = 32768 -func serverAddr() string { +func ServerAddr() string { return sharedConfig.ircHost + ":" + strconv.Itoa(sharedConfig.ircPort) } -func connectionWithNetworkConnection(conn *net.Conn) *connection { - c := connection{} +func ConnectionWithNetworkConnection(conn *net.Conn) *Session { + c := Session{} c.client = conn c.generateRandomKey() c.loadRSAKey() @@ -42,22 +60,22 @@ func connectionWithNetworkConnection(conn *net.Conn) *connection { c.trustListening = false c.trustQueue = make(chan *trustRequest, 100) - serverConn, _ := net.Dial("tcp", serverAddr()) + serverConn, _ := net.Dial("tcp", ServerAddr()) c.server = &serverConn go c.listenReadClient() go c.listenReadServer() - fmt.Println("Connection established to " + serverAddr() + " <-> " + (*conn).LocalAddr().String()) + fmt.Println("Session established to " + ServerAddr() + " <-> " + (*conn).LocalAddr().String()) return &c } -func (c *connection) close() { +func (c *Session) close() { (*c.client).Close() (*c.server).Close() - fmt.Println("Connection closed") + fmt.Println("Session closed") } func readNetConnection(conn *net.Conn) ([]byte, error) { @@ -76,7 +94,7 @@ func readNetConnection(conn *net.Conn) ([]byte, error) { return buf[:l], nil } -func (c *connection) listenReadClient() { +func (c *Session) listenReadClient() { for { buf, err := readNetConnection(c.client) if err != nil { @@ -88,7 +106,7 @@ func (c *connection) listenReadClient() { } } -func (c *connection) listenReadServer() { +func (c *Session) listenReadServer() { for { buf, err := readNetConnection(c.server) if err != nil { @@ -100,20 +118,20 @@ func (c *connection) listenReadServer() { } } -func (c *connection) readClient(buf []byte) { +func (c *Session) readClient(buf []byte) { m := messageWithBytesAndConnection(buf, c) m.encrypt() } -func (c *connection) readServer(buf []byte) { +func (c *Session) readServer(buf []byte) { m := messageWithBytesAndConnection(buf, c) m.decrypt() } -func (c connection) writeClient(buf []byte) { +func (c Session) writeClient(buf []byte) { (*c.client).Write(buf) } -func (c connection) writeServer(buf []byte) { +func (c Session) writeServer(buf []byte) { (*c.server).Write(buf) } diff --git a/e2eirc/control_commands.go b/connection/control_commands.go similarity index 83% rename from e2eirc/control_commands.go rename to connection/control_commands.go index d2569c5..a577af7 100644 --- a/e2eirc/control_commands.go +++ b/connection/control_commands.go @@ -1,4 +1,4 @@ -package e2eirc +package connection import ( "encoding/base64" @@ -7,11 +7,11 @@ import ( ) func (p *peer) recieveControlCommand(msg string) { - fmt.Println("Control command recieved!", msg) + fmt.Println("Control Command recieved!", msg) - components := strings.SplitN(msg, " ", 3) - command := components[1] - payload := components[2] +.Components := strings.SplitN(msg, " ", 3) + command :=.Components[1] + payload :=.Components[2] switch command { case "HANDSHAKE": diff --git a/connection/keys.go b/connection/keys.go new file mode 100644 index 0000000..3c72690 --- /dev/null +++ b/connection/keys.go @@ -0,0 +1,132 @@ +package connection + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + "syscall" + + "golang.org/x/crypto/ssh/terminal" + + "git.tcp.direct/tcp.direct/IR5EC/config" + "git.tcp.direct/tcp.direct/IR5EC/db" +) + +var SharedPrivateKey *rsa.PrivateKey + +func getPassword(msg string, err bool) string { + if !err && config.Opt.StorePassword != "" { + return config.Opt.StorePassword + } + fmt.Print(msg) + fmt.Print(": ") + bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) + pass := string(bytePassword) + print("\n\n") + return strings.TrimSpace(pass) +} + +func GenerateRSA() *rsa.PrivateKey { + reader := rand.Reader + bitSize := 2048 + fmt.Print("Generating " + strconv.Itoa(bitSize) + " bit private key. Please wait...") + key, _ := rsa.GenerateKey(reader, bitSize) + + blockType := "RSA PRIVATE KEY" + + cipherType := x509.PEMCipherAES256 + + fmt.Println(" Done") + + password := getPassword("Enter a new password for your private key. If you lose it you won't be able to confirm your identity on chat", false) + + encryptedPEMBlock, _ := x509.EncryptPEMBlock(rand.Reader, + blockType, + x509.MarshalPKCS1PrivateKey(key), + []byte(password), + cipherType) + + pemdata := pem.EncodeToMemory(encryptedPEMBlock) + ioutil.WriteFile(dirPath()+"/private_key.pem", []byte(pemdata), perms) + + return key +} + +func LoadOrGenerateKey(retry bool) *rsa.PrivateKey { + + notExist := db.Store.Get() + + if notExist { + SharedPrivateKey = GenerateRSA() + return SharedPrivateKey + } + + if !retry { + fmt.Print("Key found! Loading...") + } + + file, err := ioutil.ReadFile(path) + if err == nil { + block, _ := pem.Decode(file) + + if !retry { + fmt.Println(" Done") + } + + var password string + + if retry { + password = getPassword("Password Incorrect. Try Again", true) + } else { + password = getPassword("Enter Private Key Password", false) + } + + b, err := x509.DecryptPEMBlock(block, []byte(password)) + + if err != nil { + return LoadOrGenerateKey(true) + } + + key, err := x509.ParsePKCS1PrivateKey(b) + + if err == nil { + SharedPrivateKey = key + return SharedPrivateKey + } + } + } + + +} + +func (c *connection) loadRSAKey() { + if SharedPrivateKey == nil { + LoadOrGenerateKey(false) + } + + c.privateKey = SharedPrivateKey + c.publicKey = &SharedPrivateKey.PublicKey +} + +func (p *peer) setPublicKey(b64String string) { + b, err := base64.StdEncoding.DecodeString(b64String) + if err != nil { + return + } + + publicKeyI, err := x509.ParsePKIXPublicKey(b) + + if err != nil { + return + } + + publicKey := publicKeyI.(*rsa.PublicKey) + p.publicKey = publicKey +} diff --git a/connection/session.go b/connection/session.go new file mode 100644 index 0000000..10317c3 --- /dev/null +++ b/connection/session.go @@ -0,0 +1,23 @@ +package connection + +func (c *Session) loadRegistry() { + c.peerRegistry = map[string]*peer{} +} + +func (c *Session) peerWithID(id string) *peer { + peer := c.peerRegistry[id] + + if peer == nil { + peer = c.newPeerWithID(id) + } + + return peer +} + +func (c *Session) newPeerWithID(id string) *peer { + p := peer{id: id, conn: c} + p.name = id + p.unencryptedMessages = []*message{} + c.peerRegistry[id] = &p + return &p +} diff --git a/e2eirc/aes.go b/crypto/aes.go similarity index 84% rename from e2eirc/aes.go rename to crypto/aes.go index aaf7500..c45df3b 100644 --- a/e2eirc/aes.go +++ b/crypto/aes.go @@ -1,25 +1,17 @@ -package e2eirc +package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" - "fmt" "io" - "os" + + "git.tcp.direct/tcp.direct/IR5EC/connection" ) -func (c *connection) generateRandomKey() { - key := make([]byte, 32) - _, err := rand.Read(key) - - if err != nil { - fmt.Println("Fatal: Could not generate random key") - os.Exit(0) - } - - c.encryptionKey = key +func GenerateRandomKey(c *connection.Session) { + c.EncryptionKey() } func (c *connection) encryptionKeyBase64() string { diff --git a/crypto/rsa.go b/crypto/rsa.go new file mode 100644 index 0000000..884c676 --- /dev/null +++ b/crypto/rsa.go @@ -0,0 +1,79 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "fmt" + "os" +) + +func Unlock() { + LoadOrGenerateKey(false) +} + +func RSAEncrypt(key *rsa.PublicKey, buf string) string { + secretMessage := []byte(buf) + + rng := rand.Reader + + ciphertext, err := rsa.EncryptPKCS1v15(rng, key, secretMessage) + if err != nil { + fmt.Fprintf(os.Stderr, "Error from encryption: %s\n", err) + return "" + } + + return "RSA " + base64.StdEncoding.EncodeToString(ciphertext) +} + +func (c *connection) rsaDecrypt(buf string) string { + return RSADecrypt(c.privateKey, buf) +} + +func RSADecrypt(key *rsa.PrivateKey, buf string) string { + // Remove RSA Prefix + buf = buf[4:] + + b, err := base64.StdEncoding.DecodeString(buf) + + // crypto/rand.Reader is a good source of entropy for blinding the RSA + // operation. + rng := rand.Reader + + plaintext, err := rsa.DecryptPKCS1v15(rng, key, b) + if err != nil { + fmt.Fprintf(os.Stderr, "Error from decryption: %s\n", err) + return "" + } + + return string(plaintext) +} + +func (c *connection) publicKeyBase64() string { + bytes, _ := x509.MarshalPKIXPublicKey(c.publicKey) + b64 := base64.StdEncoding.EncodeToString(bytes) + + return b64 +} + +func (p *peer) publicKeyFingerprint() string { + h := sha256.New() + + bytes, _ := x509.MarshalPKIXPublicKey(p.publicKey) + h.Write(bytes) + + return hex.EncodeToString(h.Sum(nil)) +} + +func (p *peer) publicKeyFingerprintSha1() string { + h := sha1.New() + + bytes, _ := x509.MarshalPKIXPublicKey(p.publicKey) + h.Write(bytes) + + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/db/storage.go b/db/storage.go new file mode 100644 index 0000000..e0b1726 --- /dev/null +++ b/db/storage.go @@ -0,0 +1,38 @@ +package db + +import ( + "git.tcp.direct/kayos/chestnut" + "git.tcp.direct/kayos/chestnut/encryptor/aes" + "git.tcp.direct/kayos/chestnut/encryptor/crypto" + "git.tcp.direct/kayos/chestnut/log" + "git.tcp.direct/kayos/chestnut/storage/bitcask" + "github.com/awnumar/memguard" + + "git.tcp.direct/tcp.direct/IR5EC/config" +) + +var Store *chestnut.Chestnut +var enclave *memguard.Enclave + +func getSecret(Secret crypto.Secret) []byte { + o, err := enclave.Open() + if err != nil { + log.Log.Error(err) + } + o.RLock() + defer o.RUnlock() + return o.Bytes() +} + +func Initialize(path string) { + enclave = memguard.NewEnclave([]byte(config.Opt.StorePassword)) + Store = chestnut.NewChestnut( + bitcask.NewStore(path), + chestnut.WithZerologLogger(log.DebugLevel), + chestnut.WithAES( + crypto.Key256, aes.CFB, + crypto.NewSecureSecret("main", getSecret), + ), + chestnut.OverwritesForbidden(), + ) +} diff --git a/e2eirc/trust_registry.go b/db/trust_registry.go similarity index 62% rename from e2eirc/trust_registry.go rename to db/trust_registry.go index 4cacc44..1aac790 100644 --- a/e2eirc/trust_registry.go +++ b/db/trust_registry.go @@ -1,32 +1,33 @@ -package e2eirc +package db import ( "fmt" "io/ioutil" "strings" + + "git.tcp.direct/tcp.direct/IR5EC/connection" + "git.tcp.direct/tcp.direct/IR5EC/crypto" ) var trustRegistry = map[string]bool{} var trustRegistryLoaded = false func loadTrustRegistry() { - if !trustRegistryLoaded { - trustRegistryLoaded = true - path := registryPath() - file, err := ioutil.ReadFile(path) - if err == nil { - reg := rsaDecrypt(sharedPrivateKey, string(file)) - if reg != "" { - lines := strings.Split(string(reg), "\n") - for _, key := range lines { - trustRegistry[key] = true - } + if trustRegistryLoaded { + return + } + + reg := crypto.RSADecrypt(connection.SharedPrivateKey, string(file)) + if reg != "" { + lines := strings.Split(string(reg), "\n") + for _, key := range lines { + trustRegistry[key] = true } } } } -func isTrustedKey(fingerprint string) bool { +func IsTrustedKey(fingerprint string) bool { loadTrustRegistry() return trustRegistry[fingerprint] } @@ -53,7 +54,7 @@ func saveTrustRegistry() { linesString := strings.Join(lines, "\n") - data := rsaEncrypt(&sharedPrivateKey.PublicKey, linesString) + data := crypto.RSAEncrypt(&sharedPrivateKey.PublicKey, linesString) err := ioutil.WriteFile(registryPath(), []byte(data), perms) fmt.Println(err, "