feature: Initial commit.
This commit is contained in:
commit
c6c407d205
12 changed files with 1402 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
@ -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.
|
6
ascii.txt
Normal file
6
ascii.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
___. .__ __ .__ .___
|
||||
\_ |__ |__| ______ _____ __ ___/ |_| |__ __| _/
|
||||
| __ \| |/ ___// \| | \ __\ | \ / __ |
|
||||
| \_\ \ |\___ \| Y Y \ | /| | | Y \/ /_/ |
|
||||
|___ /__/____ >__|_| /____/ |__| |___| /\____ |
|
||||
\/ \/ \/ \/ \/
|
208
client/client.go
Normal file
208
client/client.go
Normal file
|
@ -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
|
||||
}
|
290
commons/conn.go
Normal file
290
commons/conn.go
Normal file
|
@ -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)
|
||||
}
|
11
commons/conv.go
Normal file
11
commons/conv.go
Normal file
|
@ -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)
|
||||
}
|
19
commons/enum.go
Normal file
19
commons/enum.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package commons
|
||||
|
||||
const (
|
||||
SendPublicKey = iota
|
||||
SwitchToSymmetricKey
|
||||
ClientSendHost
|
||||
GetSigningServers
|
||||
GetTrustedDomains
|
||||
InitiateForwarding
|
||||
)
|
||||
|
||||
const (
|
||||
XChaCha20Poly1305 = iota
|
||||
)
|
||||
|
||||
const (
|
||||
BitLimit24 = 16_777_215
|
||||
BitLimit16 = 65535
|
||||
)
|
33
go.mod
Normal file
33
go.mod
Normal file
|
@ -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
|
||||
)
|
72
go.sum
Normal file
72
go.sum
Normal file
|
@ -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=
|
331
main.go
Normal file
331
main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
257
server/server.go
Normal file
257
server/server.go
Normal file
|
@ -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
|
||||
}
|
41
tests/create_keyring.go
Normal file
41
tests/create_keyring.go
Normal file
|
@ -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
|
||||
}
|
131
tests/integration_test.go
Normal file
131
tests/integration_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue