Encrypt TCP tunnel payload with AES-GCM (#6)
CI / check-and-test (push) Failing after 10s

This commit was merged in pull request #6.
This commit is contained in:
s1d3sw1ped_bot
2026-08-31 22:30:40 -05:00
parent 1fef0b6a70
commit 283781fd47
6 changed files with 524 additions and 22 deletions
+68
View File
@@ -0,0 +1,68 @@
package encryption
import (
"encoding/binary"
"fmt"
"io"
)
// MaxFrameSize is the maximum AES-GCM ciphertext length accepted on a framed
// TCP tunnel (uint32 length prefix + ciphertext).
const MaxFrameSize = 1024 * 1024 // 1 MiB
// WriteEncryptedFrame encrypts plaintext with AES-GCM (EncryptData) and writes
// a uint32 big-endian length prefix followed by the ciphertext.
func WriteEncryptedFrame(w io.Writer, plaintext, key []byte) error {
ciphertext, err := EncryptData(plaintext, key)
if err != nil {
return err
}
if len(ciphertext) == 0 {
return fmt.Errorf("encrypted data cannot be empty")
}
if len(ciphertext) > MaxFrameSize {
return fmt.Errorf("frame too large: %d bytes (max %d)", len(ciphertext), MaxFrameSize)
}
var length [4]byte
binary.BigEndian.PutUint32(length[:], uint32(len(ciphertext)))
if err := writeFull(w, length[:]); err != nil {
return err
}
return writeFull(w, ciphertext)
}
// ReadEncryptedFrame reads a uint32 big-endian length, caps it, reads the
// ciphertext, and decrypts it with AES-GCM (DecryptData).
func ReadEncryptedFrame(r io.Reader, key []byte) ([]byte, error) {
var length uint32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return nil, err
}
if length == 0 {
return nil, fmt.Errorf("frame length cannot be zero")
}
if length > MaxFrameSize {
return nil, fmt.Errorf("frame too large: %d bytes (max %d)", length, MaxFrameSize)
}
ciphertext := make([]byte, length)
if _, err := io.ReadFull(r, ciphertext); err != nil {
return nil, fmt.Errorf("failed to read frame data: %v", err)
}
return DecryptData(ciphertext, key)
}
func writeFull(w io.Writer, data []byte) error {
for len(data) > 0 {
n, err := w.Write(data)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
data = data[n:]
}
return nil
}
+136
View File
@@ -0,0 +1,136 @@
package encryption
import (
"bytes"
"encoding/binary"
"io"
"strings"
"testing"
)
func testKey(t *testing.T) []byte {
t.Helper()
return DeriveKey("test-encryption-key-for-tcp-frames")
}
func TestEncryptedFrameRoundTrip(t *testing.T) {
key := testKey(t)
original := []byte("Hello, framed TCP tunnel!")
var buf bytes.Buffer
if err := WriteEncryptedFrame(&buf, original, key); err != nil {
t.Fatalf("WriteEncryptedFrame failed: %v", err)
}
got, err := ReadEncryptedFrame(&buf, key)
if err != nil {
t.Fatalf("ReadEncryptedFrame failed: %v", err)
}
if !bytes.Equal(got, original) {
t.Errorf("round trip mismatch: got %q want %q", got, original)
}
}
func TestEncryptedFrameMultipleChunks(t *testing.T) {
key := testKey(t)
chunks := [][]byte{
[]byte("chunk-one"),
[]byte("chunk-two-is-longer"),
[]byte{0x00, 0x01, 0xff},
}
var buf bytes.Buffer
for _, c := range chunks {
if err := WriteEncryptedFrame(&buf, c, key); err != nil {
t.Fatalf("WriteEncryptedFrame failed: %v", err)
}
}
for i, want := range chunks {
got, err := ReadEncryptedFrame(&buf, key)
if err != nil {
t.Fatalf("ReadEncryptedFrame chunk %d failed: %v", i, err)
}
if !bytes.Equal(got, want) {
t.Errorf("chunk %d mismatch: got %q want %q", i, got, want)
}
}
}
func TestEncryptedFrameRejectsZeroLength(t *testing.T) {
key := testKey(t)
var buf bytes.Buffer
if err := binary.Write(&buf, binary.BigEndian, uint32(0)); err != nil {
t.Fatal(err)
}
if _, err := ReadEncryptedFrame(&buf, key); err == nil {
t.Fatal("expected error for zero-length frame")
}
}
func TestEncryptedFrameRejectsOversizeLength(t *testing.T) {
key := testKey(t)
var buf bytes.Buffer
if err := binary.Write(&buf, binary.BigEndian, uint32(MaxFrameSize+1)); err != nil {
t.Fatal(err)
}
if _, err := ReadEncryptedFrame(&buf, key); err == nil {
t.Fatal("expected error for oversize frame length")
} else if !strings.Contains(err.Error(), "too large") {
t.Errorf("expected too-large error, got %v", err)
}
}
func TestEncryptedFrameWrongKey(t *testing.T) {
key1 := DeriveKey("tcp-frame-key-1")
key2 := DeriveKey("tcp-frame-key-2")
var buf bytes.Buffer
if err := WriteEncryptedFrame(&buf, []byte("secret"), key1); err != nil {
t.Fatal(err)
}
if _, err := ReadEncryptedFrame(&buf, key2); err == nil {
t.Fatal("decrypting with the wrong key should fail")
}
}
func TestHTTPPlaintextNotVisibleOnFramedTunnel(t *testing.T) {
key := testKey(t)
req := []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
var wire bytes.Buffer
if err := WriteEncryptedFrame(&wire, req, key); err != nil {
t.Fatalf("WriteEncryptedFrame failed: %v", err)
}
sniffed := wire.Bytes()
if bytes.Contains(sniffed, []byte("GET /")) {
t.Fatal("sniffer saw HTTP request line on the tunnel")
}
if bytes.Contains(sniffed, []byte("Host:")) {
t.Fatal("sniffer saw HTTP Host header on the tunnel")
}
if bytes.Contains(sniffed, []byte("example.com")) {
t.Fatal("sniffer saw HTTP hostname on the tunnel")
}
// Length prefix is 4 bytes; ciphertext must be longer than plaintext.
if len(sniffed) < 4+len(req) {
t.Fatalf("framed ciphertext too short: %d", len(sniffed))
}
got, err := ReadEncryptedFrame(bytes.NewReader(sniffed), key)
if err != nil {
t.Fatalf("decrypt failed: %v", err)
}
if !bytes.Equal(got, req) {
t.Errorf("decrypted HTTP mismatch: got %q", got)
}
}
func TestReadEncryptedFrameEOF(t *testing.T) {
key := testKey(t)
if _, err := ReadEncryptedFrame(bytes.NewReader(nil), key); err != io.EOF {
t.Errorf("expected io.EOF, got %v", err)
}
}