diff --git a/README.md b/README.md index 90c768b..b7ce2be 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,5 @@ _A proxy to encrypt the Traccar Freematics protocol._ +Inspired by previous work: https://github.com/rfhigler/Freematics/commit/25cf781ca9fecc3e3082348ce9d28e4d69ff7764 diff --git a/server/encryption/decrypt.go b/server/encryption/decrypt.go index 97bc7a3..232091e 100644 --- a/server/encryption/decrypt.go +++ b/server/encryption/decrypt.go @@ -17,8 +17,9 @@ func Decrypt(key, ciphertextMsg []byte) ([]byte, error) { return nil, errors.New("ciphertext too short") } - // Split nonce and ciphertext. - nonce, ciphertext, tag := ciphertextMsg[:nonceSize], ciphertextMsg[nonceSize:len(ciphertextMsg)-tagSize], ciphertextMsg[len(ciphertextMsg)-tagSize:] + // Split the message apart. + // The order is nonce, ciphertext, and tag. The last two aren't used. + nonce, _, _ := ciphertextMsg[:nonceSize], ciphertextMsg[nonceSize:len(ciphertextMsg)-tagSize], ciphertextMsg[len(ciphertextMsg)-tagSize:] - return aead.Open(nil, nonce, append(ciphertext, tag...), nil) + return aead.Open(nil, nonce, ciphertextMsg[nonceSize:], nil) } diff --git a/server/encryption/encrypt.go b/server/encryption/encrypt.go new file mode 100644 index 0000000..b1c624e --- /dev/null +++ b/server/encryption/encrypt.go @@ -0,0 +1,24 @@ +package encryption + +import ( + "crypto/rand" + "golang.org/x/crypto/chacha20poly1305" + "io" +) + +func Encrypt(key, plaintextMsg []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + // Generate a new nonce for this encryption. + nonce := make([]byte, aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // Encrypt the message and append the nonce and the ciphertext. + ciphertext := aead.Seal(nonce, nonce, plaintextMsg, nil) + return ciphertext, nil +} diff --git a/server/server.go b/server/server.go index c15f341..3493388 100644 --- a/server/server.go +++ b/server/server.go @@ -73,43 +73,91 @@ func main() { } defer conn.Close() - // Address to forward the decrypted messages - forwardAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", dest.Address, dest.Port)) - if err != nil { - logger.Fatalln("Error resolving forward address:", err) - return - } - - forwardConn, err := net.DialUDP("udp", nil, forwardAddr) - if err != nil { - logger.Fatalln("Error dialing to forward address:", err) - return - } - defer forwardConn.Close() - logger.Infof("Listening on 0.0.0.0:%s\n", port) for { - buf := make([]byte, 1500) // 1500 is the standard internet MTU + buf := make([]byte, 1500) // 1500 is the standard internet MTU. n, addr, err := conn.ReadFromUDP(buf) if err != nil { logger.Fatalf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error reading from UDP: %s", err))) - continue } - // Forward the decrypted message + // Handle the message. go func(addr *net.UDPAddr, buf []byte, n int) { - plaintext, err := encryption.Decrypt(key, buf[:n]) // Use only the part of the buffer that has data + // Do the decryption. + var plaintext []byte + if len(buf[:n]) > 0 { + plaintext, err = encryption.Decrypt(key, buf[:n]) // Use only the part of the buffer that has data. + if err != nil { + rawHex := hex.EncodeToString(buf[:n]) + logger.Warnf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf(`Error decrypting message: %s. Length: %d, Raw: "%s"`, err, len(rawHex), rawHex))) + // Forward the raw message to the backend without bothering with decryption. + plaintext = buf[:n] + } + } else { + plaintext = buf[:n] + } + + forwardAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", dest.Address, dest.Port)) if err != nil { - logger.Errorf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error decrypting message: %s", err))) + logger.Fatalln("Error resolving forward address:", err) return } + // Create a new UDP address for listening to the backend server's response. + listenAddr, err := net.ResolveUDPAddr("udp", ":0") // Let the OS pick a free port. + if err != nil { + logger.Fatalln("Error resolving listen address:", err) + return + } + + // Create a new UDP listener for the backend server's response. + listenConn, err := net.ListenUDP("udp", listenAddr) + if err != nil { + logger.Fatalln("Error listening for backend response:", err) + return + } + defer listenConn.Close() + + // Dial the backend server without binding a local address. + forwardConn, err := net.DialUDP("udp", nil, forwardAddr) + if err != nil { + logger.Fatalln("Error dialing to forward address:", err) + return + } + defer forwardConn.Close() + + // Forward the plaintext to the backend. _, err = forwardConn.Write(plaintext) if err != nil { logger.Errorf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error forwarding message: %s", err))) return } + + // Read the response from the backend. + backendResponse := make([]byte, 1500) + n, err = forwardConn.Read(backendResponse) + if err != nil { + logger.Errorf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error reading response from backend server: %s", err))) + return + } + + fmt.Println(string(backendResponse[:])) + + // Encrypt the backend's response. + encryptedBackendResponse, err := encryption.Encrypt(key, backendResponse[:n]) + if err != nil { + logger.Errorf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error encrypting response: %s", err))) + return + } + + // Forward the encrypted backend response to the client. + _, err = conn.WriteToUDP(encryptedBackendResponse, addr) + if err != nil { + logger.Errorf(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, fmt.Sprintf("Error forwarding response to client: %s", err))) + return + } + logger.Infof(formatLogMsg(addr.IP.String(), dest.Address, dest.Port, string(plaintext))) }(addr, buf, n) } diff --git a/test/test.py b/test/test.py index 46d6e4c..2060a3b 100644 --- a/test/test.py +++ b/test/test.py @@ -13,7 +13,7 @@ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) message = ('Hello, Server! ' + str(time.time())).encode() # The key and nonce -key = bytes.fromhex('example123') +key = bytes.fromhex('d38a3b96a26d0b1139bd30c174884f5dbc8eaaf492493725633ecebfa4ab19e9') # Encrypt the message cipher = ChaCha20_Poly1305.new(key=key) diff --git a/test/test_raw.py b/test/test_raw.py new file mode 100644 index 0000000..6667704 --- /dev/null +++ b/test/test_raw.py @@ -0,0 +1,36 @@ +import socket +import binascii + +from Crypto.Cipher import ChaCha20_Poly1305 + +# Send an initalization message and decrypt the response. +# Use this key: d38a3b96a26d0b1139bd30c174884f5dbc8eaaf492493725633ecebfa4ab19e9 + +# The server's address and port +server_address = ('localhost', 5171) + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +# Convert hex string to bytes +ciphertext = binascii.unhexlify('40a7a1ef284e36e65cdb87abdb9aaea7ba4df5ae527b7311ba79a7d7f73729268d5b136c0c701fe366d775315f33e9ef893214fbf26a6ec281c8eadf46663b9d90') + +# Send the encrypted message to the server +sock.sendto(ciphertext, server_address) + +# Receive the response from the server +response, server = sock.recvfrom(4096) + +# Decrypt that response +key = bytes.fromhex('d38a3b96a26d0b1139bd30c174884f5dbc8eaaf492493725633ecebfa4ab19e9') +# ChaCha20_Poly1305 nonce size is 12 bytes and tag size is 16 bytes +nonce = response[:12] +ciphertext_and_tag = response[12:] +ciphertext = ciphertext_and_tag[:-16] +tag = ciphertext_and_tag[-16:] + +cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) +plaintext = cipher.decrypt_and_verify(ciphertext, tag) + +print("Decrypted message: ", plaintext) + +sock.close()