From c6c407d2057983bc5ab08e1a584ebbf801a23e57 Mon Sep 17 00:00:00 2001 From: greysoh Date: Sat, 19 Oct 2024 14:49:24 -0400 Subject: [PATCH] feature: Initial commit. --- README.md | 3 + ascii.txt | 6 + client/client.go | 208 ++++++++++++++++++++++++ commons/conn.go | 290 +++++++++++++++++++++++++++++++++ commons/conv.go | 11 ++ commons/enum.go | 19 +++ go.mod | 33 ++++ go.sum | 72 +++++++++ main.go | 331 ++++++++++++++++++++++++++++++++++++++ server/server.go | 257 +++++++++++++++++++++++++++++ tests/create_keyring.go | 41 +++++ tests/integration_test.go | 131 +++++++++++++++ 12 files changed, 1402 insertions(+) create mode 100644 README.md create mode 100644 ascii.txt create mode 100644 client/client.go create mode 100644 commons/conn.go create mode 100644 commons/conv.go create mode 100644 commons/enum.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 server/server.go create mode 100644 tests/create_keyring.go create mode 100644 tests/integration_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..0421a51 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Bismuth Protocol + +The Bismuth protocol is a thin wrapper for any protocol that adds TLS-like features, without being TLS on its own. diff --git a/ascii.txt b/ascii.txt new file mode 100644 index 0000000..85f5e99 --- /dev/null +++ b/ascii.txt @@ -0,0 +1,6 @@ +___. .__ __ .__ .___ +\_ |__ |__| ______ _____ __ ___/ |_| |__ __| _/ + | __ \| |/ ___// \| | \ __\ | \ / __ | + | \_\ \ |\___ \| Y Y \ | /| | | Y \/ /_/ | + |___ /__/____ >__|_| /____/ |__| |___| /\____ | + \/ \/ \/ \/ \/ diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..5a210f0 --- /dev/null +++ b/client/client.go @@ -0,0 +1,208 @@ +package client + +import ( + "crypto/cipher" + "crypto/rand" + "fmt" + "net" + + core "git.greysoh.dev/imterah/bismuthd/commons" + "golang.org/x/crypto/chacha20poly1305" + + "github.com/ProtonMail/gopenpgp/v3/crypto" +) + +func (bismuth BismuthClient) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) { + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead()) + + if _, err := rand.Read(nonce); err != nil { + return []byte{}, err + } + + encryptedMsg := aead.Seal(nonce, nonce, msg, nil) + return encryptedMsg, nil +} + +func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) { + if len(encMsg) < aead.NonceSize() { + return []byte{}, fmt.Errorf("ciphertext too short") + } + + // Split nonce and ciphertext. + nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():] + + // Decrypt the message and check it wasn't tampered with. + decryptedData, err := aead.Open(nil, nonce, ciphertext, nil) + + if err != nil { + return []byte{}, err + } + + return decryptedData, nil +} + +type BismuthClient struct { + PublicKey *crypto.Key + PrivateKey *crypto.Key + + pgp *crypto.PGPHandle +} + +func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) { + // Yes, I'm aware defer exists. It won't work if I use it in this context. I'll shank anyone that complains + // Exchange our public keys first + + ownKey, err := bismuth.PublicKey.GetPublicKey() + + if err != nil { + conn.Close() + return nil, err + } + + pubKeyLengthBytes := make([]byte, 3) + pubKeyLength := uint32(len(ownKey)) + core.Int32ToInt24(pubKeyLengthBytes, pubKeyLength) + + conn.Write([]byte{core.SendPublicKey}) + conn.Write(pubKeyLengthBytes) + conn.Write(ownKey) + + messageMode := make([]byte, 1) + + if _, err = conn.Read(messageMode); err != nil { + conn.Close() + return nil, err + } + + if messageMode[0] != core.SendPublicKey { + conn.Close() + return nil, fmt.Errorf("server failed to return its public key") + } + + if _, err = conn.Read(pubKeyLengthBytes); err != nil { + conn.Close() + return nil, err + } + + pubKeyLength = core.Int24ToInt32(pubKeyLengthBytes) + pubKeyBytes := make([]byte, pubKeyLength) + + if _, err = conn.Read(pubKeyBytes); err != nil { + conn.Close() + return nil, err + } + + if _, err = crypto.NewKey(pubKeyBytes); err != nil { + conn.Close() + return nil, err + } + + // Then exchange the symmetric key + + conn.Write([]byte{core.SwitchToSymmetricKey}) + + if _, err = conn.Read(messageMode); err != nil { + conn.Close() + return nil, err + } + + if messageMode[0] != core.SwitchToSymmetricKey { + conn.Close() + return nil, fmt.Errorf("server failed to return symmetric key") + } + + encryptedSymmKeyLengthInBytes := make([]byte, 3) + + if _, err = conn.Read(encryptedSymmKeyLengthInBytes); err != nil { + conn.Close() + return nil, err + } + + encryptedSymmKeyLength := core.Int24ToInt32(encryptedSymmKeyLengthInBytes) + encryptedSymmKey := make([]byte, encryptedSymmKeyLength) + + if _, err = conn.Read(encryptedSymmKey); err != nil { + conn.Close() + return nil, err + } + + decHandleForSymmKey, err := bismuth.pgp.Decryption().DecryptionKey(bismuth.PrivateKey).New() + + if err != nil { + return nil, err + } + + decryptedSymmKey, err := decHandleForSymmKey.Decrypt(encryptedSymmKey, crypto.Bytes) + + if err != nil { + return nil, err + } + + symmKeyInfo := decryptedSymmKey.Bytes() + + if symmKeyInfo[0] != core.XChaCha20Poly1305 { + conn.Close() + return nil, fmt.Errorf("unsupported encryption method recieved") + } + + symmKey := symmKeyInfo[1 : chacha20poly1305.KeySize+1] + aead, err := chacha20poly1305.NewX(symmKey) + + // Start proxying + + startForwardingPacket := []byte{ + core.InitiateForwarding, + } + + encryptedForwardPacket, err := bismuth.encryptMessage(aead, startForwardingPacket) + + if err != nil { + conn.Close() + return nil, err + } + + encryptedForwardPacketPacketSize := make([]byte, 3) + + core.Int32ToInt24(encryptedForwardPacketPacketSize, uint32(len(encryptedForwardPacket))) + + conn.Write(encryptedForwardPacketPacketSize) + conn.Write(encryptedForwardPacket) + + _, err = bismuth.decryptMessage(aead, encryptedForwardPacket) + + bmConn := core.BismuthConn{ + Aead: aead, + PassedConn: conn, + MaxBufSize: core.ConnStandardMaxBufSize, + } + + bmConn.DoInitSteps() + + return core.BismuthConnWrapped{ + Bismuth: &bmConn, + }, nil +} + +func New(pubKey string, privKey string) (*BismuthClient, error) { + publicKey, err := crypto.NewKeyFromArmored(pubKey) + + if err != nil { + return nil, err + } + + privateKey, err := crypto.NewKeyFromArmored(privKey) + + if err != nil { + return nil, err + } + + pgp := crypto.PGP() + + bismuth := BismuthClient{ + PublicKey: publicKey, + PrivateKey: privateKey, + pgp: pgp, + } + + return &bismuth, nil +} diff --git a/commons/conn.go b/commons/conn.go new file mode 100644 index 0000000..082705b --- /dev/null +++ b/commons/conn.go @@ -0,0 +1,290 @@ +package commons + +import ( + "crypto/cipher" + "crypto/rand" + "fmt" + "net" + "sync" + "time" +) + +// Give it 2x the max size for a TCP packet +var ConnStandardMaxBufSize = (65535 * 2) +var CryptHeader = 43 + +// Wild +type BismuthConn struct { + Aead cipher.AEAD + PassedConn net.Conn + + lock *sync.Mutex + + initDone bool + + contentBuf []byte + contentBufPos int + contentBufSize int + + MaxBufSize int + AllowNonstandardPacketSizeLimit bool + + net.Conn +} + +func (bmConn *BismuthConn) DoInitSteps() { + bmConn.lock = &sync.Mutex{} + bmConn.contentBuf = make([]byte, bmConn.MaxBufSize) + + bmConn.initDone = true +} + +func (bmConn *BismuthConn) encryptMessage(msg []byte) ([]byte, error) { + nonce := make([]byte, bmConn.Aead.NonceSize(), bmConn.Aead.NonceSize()+len(msg)+bmConn.Aead.Overhead()) + + if _, err := rand.Read(nonce); err != nil { + return []byte{}, err + } + + encryptedMsg := bmConn.Aead.Seal(nonce, nonce, msg, nil) + return encryptedMsg, nil +} + +func (bmConn *BismuthConn) decryptMessage(encMsg []byte) ([]byte, error) { + if len(encMsg) < bmConn.Aead.NonceSize() { + return []byte{}, fmt.Errorf("ciphertext too short") + } + + // Split nonce and ciphertext. + nonce, ciphertext := encMsg[:bmConn.Aead.NonceSize()], encMsg[bmConn.Aead.NonceSize():] + + // Decrypt the message and check it wasn't tampered with. + decryptedData, err := bmConn.Aead.Open(nil, nonce, ciphertext, nil) + + if err != nil { + return []byte{}, err + } + + return decryptedData, nil +} + +func (bmConn *BismuthConn) ResizeContentBuf() error { + if !bmConn.initDone { + return fmt.Errorf("bmConn not initialized") + } + + bmConn.lock.Lock() + + if bmConn.contentBufSize != 0 { + // TODO: switch this to do append() instead, when I finally decide to consider this "optimization" in the main buffer logic + // This code below basically, instead of growing it, gets the actually unused cache data, then grows it and copies it over. + // + // This is probably unneccesary, but it saves some hassle I guess. + currentContentBufData := bmConn.contentBuf[bmConn.contentBufPos:bmConn.contentBufSize] + bmConn.contentBufSize = bmConn.contentBufSize - bmConn.contentBufPos + bmConn.contentBufPos = 0 + + bmConn.contentBuf = make([]byte, bmConn.MaxBufSize) + copy(bmConn.contentBuf[len(currentContentBufData):], currentContentBufData) + } else { + bmConn.contentBuf = make([]byte, bmConn.MaxBufSize) + } + + bmConn.lock.Unlock() + + return nil +} + +func (bmConn *BismuthConn) ReadFromBuffer(b []byte) (n int, err error) { + bmConn.lock.Lock() + defer bmConn.lock.Unlock() + + calcContentBufSize := bmConn.contentBufSize - bmConn.contentBufPos + providedBufferSize := len(b) + + if bmConn.contentBufSize == 0 { + return 0, nil + } + + if calcContentBufSize <= providedBufferSize { + copy(b, bmConn.contentBuf[bmConn.contentBufPos:bmConn.contentBufSize]) + + bmConn.contentBufPos = 0 + bmConn.contentBufSize = 0 + bmConn.contentBuf = make([]byte, bmConn.MaxBufSize) + + return calcContentBufSize, nil + } else if calcContentBufSize > providedBufferSize { + newContentBufSize := bmConn.contentBufPos + providedBufferSize + copy(b, bmConn.contentBuf[bmConn.contentBufPos:newContentBufSize]) + + bmConn.contentBufPos = newContentBufSize + + return providedBufferSize, nil + } + + return 0, nil +} + +func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) { + bmConn.lock.Lock() + defer bmConn.lock.Unlock() + + bufferSize := len(b) + + encryptedContentLengthBytes := make([]byte, 3) + + if _, err = bmConn.PassedConn.Read(encryptedContentLengthBytes); err != nil { + return 0, err + } + + encryptedContentLength := Int24ToInt32(encryptedContentLengthBytes) + encryptedContent := make([]byte, encryptedContentLength) + + // Check to see if we can fit the packet inside either: + // - the max TCP packet size (64k) if 'AllowNonstandardPacketSizeLimit' isn't set; or + // - the max buffer size if 'AllowNonstandardPacketSizeLimit' is set + // We check AFTER we read to make sure that we don't corrupt any future packets, because if we don't read the packet, + // it will think that the actual packet will be the start of the packet, and that would cause loads of problems. + if !bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(65535+CryptHeader) { + return 0, fmt.Errorf("packet too large") + } else if bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(bmConn.MaxBufSize) { + return 0, fmt.Errorf("packet too large") + } + + totalPosition := 0 + + for totalPosition != int(encryptedContentLength) { + currentPosition, err := bmConn.PassedConn.Read(encryptedContent[totalPosition:encryptedContentLength]) + totalPosition += currentPosition + + if err != nil { + return 0, err + } + } + + decryptedContent, err := bmConn.decryptMessage(encryptedContent) + decryptedContentSize := len(decryptedContent) + + calcSize := min(decryptedContentSize, bufferSize) + copy(b[:calcSize], decryptedContent) + + if bufferSize < int(decryptedContentSize) { + newSlice := decryptedContent[calcSize:] + + if bmConn.contentBufSize+len(newSlice) > bmConn.MaxBufSize { + return 0, fmt.Errorf("ran out of room in the buffer to store data! (can't overflow the buffer...)") + } + + copy(bmConn.contentBuf[bmConn.contentBufSize:bmConn.contentBufSize+len(newSlice)], newSlice) + bmConn.contentBufSize += len(newSlice) + } + + if err != nil { + return calcSize, err + } + + return calcSize, nil +} + +func (bmConn *BismuthConn) Read(b []byte) (n int, err error) { + if !bmConn.initDone { + return 0, fmt.Errorf("bmConn not initialized") + } + + bufferReadSize, err := bmConn.ReadFromBuffer(b) + + if err != nil { + return bufferReadSize, err + } + + if bufferReadSize == len(b) { + return bufferReadSize, nil + } + + networkReadSize, err := bmConn.ReadFromNetwork(b[bufferReadSize:]) + + if err != nil { + return bufferReadSize + networkReadSize, err + } + + return bufferReadSize + networkReadSize, nil +} + +func (bmConn *BismuthConn) Write(b []byte) (n int, err error) { + encryptedMessage, err := bmConn.encryptMessage(b) + + if err != nil { + return 0, err + } + + encryptedMessageSize := make([]byte, 3) + Int32ToInt24(encryptedMessageSize, uint32(len(encryptedMessage))) + + bmConn.PassedConn.Write(encryptedMessageSize) + bmConn.PassedConn.Write(encryptedMessage) + + return len(b), nil +} + +func (bmConn *BismuthConn) Close() error { + return bmConn.PassedConn.Close() +} + +func (bmConn *BismuthConn) LocalAddr() net.Addr { + return bmConn.PassedConn.LocalAddr() +} + +func (bmConn *BismuthConn) RemoteAddr() net.Addr { + return bmConn.PassedConn.RemoteAddr() +} + +func (bmConn *BismuthConn) SetDeadline(time time.Time) error { + return bmConn.PassedConn.SetDeadline(time) +} + +func (bmConn *BismuthConn) SetReadDeadline(time time.Time) error { + return bmConn.PassedConn.SetReadDeadline(time) +} + +func (bmConn *BismuthConn) SetWriteDeadline(time time.Time) error { + return bmConn.PassedConn.SetWriteDeadline(time) +} + +// TODO: remove this ugly hack if possible! There's probably a better way around this... + +type BismuthConnWrapped struct { + Bismuth *BismuthConn +} + +func (bmConn BismuthConnWrapped) Read(b []byte) (n int, err error) { + return bmConn.Bismuth.Read(b) +} + +func (bmConn BismuthConnWrapped) Write(b []byte) (n int, err error) { + return bmConn.Bismuth.Write(b) +} + +func (bmConn BismuthConnWrapped) Close() error { + return bmConn.Bismuth.Close() +} + +func (bmConn BismuthConnWrapped) LocalAddr() net.Addr { + return bmConn.Bismuth.LocalAddr() +} + +func (bmConn BismuthConnWrapped) RemoteAddr() net.Addr { + return bmConn.Bismuth.RemoteAddr() +} + +func (bmConn BismuthConnWrapped) SetDeadline(time time.Time) error { + return bmConn.Bismuth.SetDeadline(time) +} + +func (bmConn BismuthConnWrapped) SetReadDeadline(time time.Time) error { + return bmConn.Bismuth.SetReadDeadline(time) +} + +func (bmConn BismuthConnWrapped) SetWriteDeadline(time time.Time) error { + return bmConn.Bismuth.SetWriteDeadline(time) +} diff --git a/commons/conv.go b/commons/conv.go new file mode 100644 index 0000000..bc81bf0 --- /dev/null +++ b/commons/conv.go @@ -0,0 +1,11 @@ +package commons + +func Int24ToInt32(b []byte) uint32 { + return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 +} + +func Int32ToInt24(b []byte, int uint32) { + b[0] = uint8((int >> 16) & 0xff) + b[1] = uint8((int >> 8) & 0xff) + b[2] = uint8(int & 0xff) +} diff --git a/commons/enum.go b/commons/enum.go new file mode 100644 index 0000000..5cd9ded --- /dev/null +++ b/commons/enum.go @@ -0,0 +1,19 @@ +package commons + +const ( + SendPublicKey = iota + SwitchToSymmetricKey + ClientSendHost + GetSigningServers + GetTrustedDomains + InitiateForwarding +) + +const ( + XChaCha20Poly1305 = iota +) + +const ( + BitLimit24 = 16_777_215 + BitLimit16 = 65535 +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7fce4de --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module git.greysoh.dev/imterah/bismuthd + +go 1.23.0 + +require ( + github.com/ProtonMail/gopenpgp/v3 v3.0.0-beta.1-proton + github.com/charmbracelet/log v0.4.0 + github.com/urfave/cli/v2 v2.27.4 + tailscale.com v1.74.1 +) + +require ( + github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.3.2 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect + golang.org/x/sys v0.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6a91404 --- /dev/null +++ b/go.sum @@ -0,0 +1,72 @@ +github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo= +github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/gopenpgp/v3 v3.0.0-beta.1-proton h1:WwNSUldLJ2Di+gvRK8ePCsY1HEgieVGyWhD90MGaSak= +github.com/ProtonMail/gopenpgp/v3 v3.0.0-beta.1-proton/go.mod h1:TBpqWZ9IzA7g3TEzNA9Fwv/nA/eYpjcvYQBq+FX+tE4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= +github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 h1:xcuWappghOVI8iNWoF2OKahVejd1LSVi/v4JED44Amo= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tailscale.com v1.74.1 h1:qhhkN+0gFZasczi+0n0eBxwfP/ZaUr+05cWdsOQ3GT0= +tailscale.com v1.74.1/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ded7a1 --- /dev/null +++ b/main.go @@ -0,0 +1,331 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "net" + "os" + "strings" + + "git.greysoh.dev/imterah/bismuthd/client" + core "git.greysoh.dev/imterah/bismuthd/commons" + "git.greysoh.dev/imterah/bismuthd/server" + "github.com/charmbracelet/log" + "github.com/urfave/cli/v2" + "tailscale.com/net/socks5" +) + +//go:embed ascii.txt +var asciiArt string + +func bismuthClientEntrypoint(cCtx *cli.Context) error { + pubKeyFile, err := os.ReadFile(cCtx.String("pubkey")) + + if err != nil { + return err + } + + privKeyFile, err := os.ReadFile(cCtx.String("privkey")) + + if err != nil { + return err + } + + pubKey := string(pubKeyFile) + privKey := string(privKeyFile) + + bismuth, err := client.New(pubKey, privKey) + + if err != nil { + return err + } + + routeAllTraffic := cCtx.Bool("route-all-traffic") + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("ip"), cCtx.String("port"))) + + if err != nil { + return err + } + + socksServer := socks5.Server{ + Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { + ip := addr[:strings.Index(addr, ":")] + + isBismuthTLD := strings.HasSuffix(ip, ".bismuth") + + // There isn't a good way to make the rest of the internet compatible and also have a seperate protocol, + // so we just do this. + if isBismuthTLD || routeAllTraffic { + log.Debugf("Recieved bismuth connection to '%s'. Routing", addr) + + address := addr + ip := ip + port := "" + + if isBismuthTLD { + ip = ip[:strings.LastIndex(addr, ".")] + port := addr[strings.Index(addr, ":")+1:] + + address = ip + ":" + port + } else { + ip = ip[:strings.LastIndex(addr, ":")] + port := addr[strings.Index(addr, ":")+1:] + + address = ip + ":" + port + } + + conn, err := net.Dial(network, address) + + if err != nil && err.Error() != "EOF" { + log.Errorf("TCP connection to '%s:%s' failed", ip, port) + return nil, err + } + + conn, err = bismuth.Conn(conn) + + if err != nil && err.Error() != "EOF" { + log.Errorf("failed to initialize bismuth connection to '%s:%s': '%s'", ip, port, err.Error()) + return nil, err + } + + return conn, err + } else { + conn, err := net.Dial(network, addr) + + if err != nil && err.Error() != "EOF" { + log.Errorf("TCP connection to '%s' failed", addr) + return nil, err + } + + return conn, err + } + }, + } + + log.Info("Bismuth client is listening...") + + socksServer.Serve(listener) + + return nil +} + +func bismuthServerEntrypoint(cCtx *cli.Context) error { + relayServers := []string{} + + pubKeyFile, err := os.ReadFile(cCtx.String("pubkey")) + + if err != nil { + return err + } + + privKeyFile, err := os.ReadFile(cCtx.String("privkey")) + + if err != nil { + return err + } + + pubKey := string(pubKeyFile) + privKey := string(privKeyFile) + + network := fmt.Sprintf("%s:%s", cCtx.String("source-ip"), cCtx.String("source-port")) + + bismuth, err := server.NewBismuthServer(pubKey, privKey, relayServers, core.XChaCha20Poly1305, func(connBismuth net.Conn) error { + connDialed, err := net.Dial("tcp", network) + + if err != nil { + return err + } + + bismuthBuffer := make([]byte, 65535) + dialBuffer := make([]byte, 65535) + + go func() { + defer connDialed.Close() + defer connBismuth.Close() + + for { + len, err := connBismuth.Read(bismuthBuffer) + + if err != nil { + log.Errorf("failed to read from bismuth server: '%s'", err.Error()) + return + } + + _, err = connDialed.Write(bismuthBuffer[:len]) + + if err != nil { + log.Errorf("failed to write to the proxied server: '%s'", err.Error()) + return + } + } + }() + + go func() { + defer connDialed.Close() + defer connBismuth.Close() + + for { + len, err := connDialed.Read(dialBuffer) + + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to read from proxied server: '%s'", err.Error()) + return + } + + _, err = connBismuth.Write(dialBuffer[:len]) + + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to write to bismuth server: '%s'", err.Error()) + return + } + } + }() + + return nil + }) + + if err != nil { + return err + } + + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("dest-ip"), cCtx.String("dest-port"))) + + if err != nil { + return err + } + + defer listener.Close() + + log.Info("Bismuth server is listening...") + + for { + conn, err := listener.Accept() + log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String()) + + if err != nil { + log.Warn(err.Error()) + continue + } + + go func() { + err := bismuth.HandleProxy(conn) + + if err != nil && err.Error() != "EOF" { + log.Warnf("Connection crashed/dropped during proxy handling: '%s'", err.Error()) + } + }() + } +} + +func main() { + fmt.Println(asciiArt) + fmt.Print("Implementation of the Bismuth protocol\n\n") + + logLevel := os.Getenv("BISMUTHD_LOG_LEVEL") + + if logLevel != "" { + switch logLevel { + case "debug": + log.SetLevel(log.DebugLevel) + + case "info": + log.SetLevel(log.InfoLevel) + + case "warn": + log.SetLevel(log.WarnLevel) + + case "error": + log.SetLevel(log.ErrorLevel) + + case "fatal": + log.SetLevel(log.FatalLevel) + } + } + + app := &cli.App{ + Name: "bismuthd", + Usage: "reference implementation of the bismuth protocol", + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + Name: "client", + Aliases: []string{"c"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pubkey", + Usage: "path to PGP public key", + Required: true, + }, + &cli.StringFlag{ + Name: "privkey", + Usage: "path to PGP private key", + Required: true, + }, + &cli.StringFlag{ + Name: "ip", + Usage: "IP to listen on for SOCKS5 server", + Value: "127.0.0.1", + }, + &cli.StringFlag{ + Name: "port", + Usage: "port to listen on for SOCKS5 server", + Value: "1080", + }, + &cli.BoolFlag{ + Name: "disable-gui", + Usage: "if set, disables the GUI and automatically accepts all certificates (not recommended)", + }, + &cli.BoolFlag{ + Name: "route-all-traffic", + Usage: "if set, routes all traffic through Bismuth, instead of just IPs that end with the fictional TLD '.bismuth'", + }, + }, + Usage: "client for the Bismuth protocol", + Action: bismuthClientEntrypoint, + }, + { + Name: "server", + Aliases: []string{"s"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pubkey", + Usage: "path to PGP public key", + Required: true, + }, + &cli.StringFlag{ + Name: "privkey", + Usage: "path to PGP private key", + Required: true, + }, + &cli.StringFlag{ + Name: "source-ip", + Usage: "IP to connect to", + Required: true, + }, + &cli.StringFlag{ + Name: "source-port", + Usage: "port to connect to", + Required: true, + }, + &cli.StringFlag{ + Name: "dest-ip", + Usage: "IP to listen on", + Value: "0.0.0.0", + }, + &cli.StringFlag{ + Name: "dest-port", + Usage: "port to listen on", + Required: true, + }, + }, + Usage: "server for the Bismuth protocol", + Action: bismuthServerEntrypoint, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..ab986f4 --- /dev/null +++ b/server/server.go @@ -0,0 +1,257 @@ +package server + +import ( + "fmt" + "net" + + core "git.greysoh.dev/imterah/bismuthd/commons" + + "crypto/cipher" + "crypto/rand" + + "github.com/ProtonMail/gopenpgp/v3/crypto" + "golang.org/x/crypto/chacha20poly1305" +) + +type BismuthServer struct { + PublicKey *crypto.Key + PrivateKey *crypto.Key + + pgp *crypto.PGPHandle + + SymmetricEncryptionAlgorithm int + + SigningServers []string + + // This is what's called after a successful handshake & connection. + HandleConnection func(conn net.Conn) error +} + +func (bismuth BismuthServer) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) { + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead()) + + if _, err := rand.Read(nonce); err != nil { + return []byte{}, err + } + + encryptedMsg := aead.Seal(nonce, nonce, msg, nil) + return encryptedMsg, nil +} + +func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) { + if len(encMsg) < aead.NonceSize() { + return []byte{}, fmt.Errorf("ciphertext too short") + } + + // Split nonce and ciphertext. + nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():] + + // Decrypt the message and check it wasn't tampered with. + decryptedData, err := aead.Open(nil, nonce, ciphertext, nil) + + if err != nil { + return []byte{}, err + } + + return decryptedData, nil +} + +// This is what's called to handle a connnection for Bismuth. +func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { + serverState := "keyHandshake" + + var clientPublicKey *crypto.Key + var aead cipher.AEAD + + for { + if serverState == "keyHandshake" { + dataModeByteArr := make([]byte, 1) + _, err := conn.Read(dataModeByteArr) + + dataMode := dataModeByteArr[0] + + if err != nil { + conn.Close() + return err + } + + if dataMode == core.SendPublicKey { + pubKeyLengthBytes := make([]byte, 3) + _, err := conn.Read(pubKeyLengthBytes) + + if err != nil { + conn.Close() + return err + } + + pubKeyLength := core.Int24ToInt32(pubKeyLengthBytes) + + publicKey := make([]byte, pubKeyLength) + _, err = conn.Read(publicKey) + + pubKey := publicKey[:] + + // Attempt to parse the public key + clientPublicKey, err = crypto.NewKey(pubKey) + + if err != nil { + conn.Close() + return err + } + + // Now, we send our key + pubKey, err = bismuth.PublicKey.GetPublicKey() + + if err != nil { + conn.Close() + return err + } + + pubKeyLength = uint32(len(pubKey)) + core.Int32ToInt24(pubKeyLengthBytes, pubKeyLength) + + conn.Write([]byte{core.SendPublicKey}) + conn.Write(pubKeyLengthBytes) + conn.Write([]byte(pubKey)) + + serverState = "symmHandshake" + } + } else if serverState == "symmHandshake" { + dataModeByteArr := make([]byte, 1) + _, err := conn.Read(dataModeByteArr) + + dataMode := dataModeByteArr[0] + + if err != nil { + conn.Close() + return err + } + + if dataMode == core.SwitchToSymmetricKey { + // TODO: Make this not hard-coded + symmetricKey := make([]byte, chacha20poly1305.KeySize) + + encryptionData := []byte{ + byte(bismuth.SymmetricEncryptionAlgorithm), + } + + if _, err := rand.Read(symmetricKey); err != nil { + conn.Close() + return err + } + + encryptionData = append(encryptionData, symmetricKey...) + + encHandle, err := bismuth.pgp.Encryption().Recipient(clientPublicKey).New() + + if err != nil { + conn.Close() + return err + } + + pgpMessage, err := encHandle.Encrypt(encryptionData) + + if err != nil { + conn.Close() + return err + } + + encryptedMessage := pgpMessage.Bytes() + gpgMessageLengthBytes := make([]byte, 3) + + core.Int32ToInt24(gpgMessageLengthBytes, uint32(len(encryptedMessage))) + + conn.Write([]byte{core.SwitchToSymmetricKey}) + conn.Write(gpgMessageLengthBytes) + conn.Write(encryptedMessage) + + aead, err = chacha20poly1305.NewX(symmetricKey) + + if err != nil { + conn.Close() + return err + } + + serverState = "APITransmit" + } + } else if serverState == "APITransmit" { + // Currently the API is in a skeleton-like state, so the API is a bit of a no-op right now. + // Fuck you Stari. + + packetSizeByteArr := make([]byte, 3) + + if _, err := conn.Read(packetSizeByteArr); err != nil { + conn.Close() + return err + } + + packetSize := core.Int24ToInt32(packetSizeByteArr) + encryptedPacket := make([]byte, packetSize) + + packetSizeInt := int(packetSize) + + totalPositionRead := 0 + + for packetSizeInt != totalPositionRead { + currentPosition, err := conn.Read(encryptedPacket[totalPositionRead:packetSizeInt]) + totalPositionRead += currentPosition + + if err != nil { + conn.Close() + return err + } + } + + packet, err := bismuth.decryptMessage(aead, encryptedPacket) + + if err != nil { + conn.Close() + return err + } + + // Fuck it, we ball + if packet[0] == core.InitiateForwarding { + bmConn := core.BismuthConn{ + Aead: aead, + PassedConn: conn, + MaxBufSize: core.ConnStandardMaxBufSize, + } + + bmConn.DoInitSteps() + + err := bismuth.HandleConnection(core.BismuthConnWrapped{ + Bismuth: &bmConn, + }) + + return err + } + } + } +} + +func NewBismuthServer(pubKey string, privKey string, signServers []string, encryptionAlgo int, connHandler func(conn net.Conn) error) (*BismuthServer, error) { + publicKey, err := crypto.NewKeyFromArmored(pubKey) + + if err != nil { + return nil, err + } + + privateKey, err := crypto.NewKeyFromArmored(privKey) + + if err != nil { + return nil, err + } + + pgp := crypto.PGP() + + bismuth := BismuthServer{ + PublicKey: publicKey, + PrivateKey: privateKey, + HandleConnection: connHandler, + SigningServers: signServers, + SymmetricEncryptionAlgorithm: encryptionAlgo, + pgp: pgp, + } + + return &bismuth, nil +} diff --git a/tests/create_keyring.go b/tests/create_keyring.go new file mode 100644 index 0000000..1e8937b --- /dev/null +++ b/tests/create_keyring.go @@ -0,0 +1,41 @@ +package bismuthd_test + +import ( + "github.com/ProtonMail/gopenpgp/v3/crypto" + "github.com/ProtonMail/gopenpgp/v3/profile" +) + +// Creates an armored GPG keyring +// Argument order is public key, then private key +func CreateKeyring(name, email string) (string, string, error) { + pgp := crypto.PGPWithProfile(profile.RFC9580()) + + privateKey, err := pgp.KeyGeneration(). + AddUserId(name, email). + New(). + GenerateKey() + + if err != nil { + return "", "", err + } + + publicKey, err := privateKey.ToPublic() + + if err != nil { + return "", "", err + } + + privateKeyArmored, err := privateKey.Armor() + + if err != nil { + return "", "", err + } + + publicKeyArmored, err := publicKey.Armor() + + if err != nil { + return "", "", err + } + + return publicKeyArmored, privateKeyArmored, nil +} diff --git a/tests/integration_test.go b/tests/integration_test.go new file mode 100644 index 0000000..388b18a --- /dev/null +++ b/tests/integration_test.go @@ -0,0 +1,131 @@ +package bismuthd_test + +import ( + "bytes" + mathRand "math/rand" + + "crypto/rand" + "net" + "strconv" + "testing" + + "git.greysoh.dev/imterah/bismuthd/client" + "git.greysoh.dev/imterah/bismuthd/commons" + "git.greysoh.dev/imterah/bismuthd/server" +) + +var testProtocolTxRxBufCount = 32 + +// Tests protocol transmitting and receiving +// This is designed to be a nightmare scenario for the protocol to push the limits on what would be possible. +func TestProtocolTxRx(t *testing.T) { + pubKeyCli, privKeyCli, err := CreateKeyring("alice", "alice@contoso.com") + + if err != nil { + t.Fatalf("failed to generate 1st pair of keys (%s)", err.Error()) + } + + pubKeyServ, privKeyServ, err := CreateKeyring("bob", "bob@contoso.com") + + if err != nil { + t.Fatalf("failed to generate 2nd pair of keys (%s)", err.Error()) + } + + t.Log("created keyrings") + + randomDataSlices := [][]byte{} + + for range testProtocolTxRxBufCount { + randomData := make([]byte, 65535) + _, err = rand.Read(randomData) + + if err != nil { + t.Fatalf("failed to generate random data (%s)", err.Error()) + } + + randomDataSlices = append(randomDataSlices, randomData) + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") + + if err != nil { + t.Fatalf("failed to listen on TCP for localhost (%s)", err.Error()) + } + + bismuth, err := server.NewBismuthServer(pubKeyServ, privKeyServ, []string{}, commons.XChaCha20Poly1305, func(conn net.Conn) error { + for entryCount, randomDataSlice := range randomDataSlices { + _, err = conn.Write(randomDataSlice) + + if err != nil { + t.Fatalf("failed to send randomDataSlice entry #%d (%s)", entryCount+1, err.Error()) + } + } + + return nil + }) + + // TODO: fix these warnings? + go func() { + conn, err := listener.Accept() + + if err != nil { + t.Fatalf("failed to accept connection from listener (%s)", err.Error()) + } + + err = bismuth.HandleProxy(conn) + + if err != nil { + t.Fatalf("failed to handle proxy in Bismuth (%s)", err.Error()) + } + }() + + port := listener.Addr().(*net.TCPAddr).Port + bismuthClient, err := client.New(pubKeyCli, privKeyCli) + + if err != nil { + t.Fatalf("failed to initialize bismuthClient (%s)", err.Error()) + } + + originalConn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port)) + + if err != nil { + t.Fatalf("failed to connect to bismuth server (%s)", err.Error()) + } + + conn, err := bismuthClient.Conn(originalConn) + + if err != nil { + t.Fatalf("bismuth client failed to handshake when connecting to server (%s)", err.Error()) + } + + // Now the real fun begins + + for index, realBuffer := range randomDataSlices { + bufferSize := len(realBuffer) + + totalBufferRead := 0 + readBuffer := make([]byte, bufferSize) + + for totalBufferRead != bufferSize { + randBufSize := mathRand.Intn(bufferSize - totalBufferRead) + + if randBufSize == bufferSize || randBufSize == 0 { + continue + } + + actualReadSize, err := conn.Read(readBuffer[totalBufferRead:]) + + if err != nil { + t.Fatalf("bismuth client failed to read in random slice #%d (%s)", index+1, err.Error()) + } + + totalBufferRead += actualReadSize + } + + if !bytes.Equal(realBuffer, readBuffer) { + t.Fatalf("buffers are different (in random slice #%d)", index+1) + } + + t.Logf("buffer #%d passed!", index+1) + } +}