Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
3.3 KB go
/* SPDX-License-Identifier: MIT
 *
 * Phobos
 */

package cobf

import (
	"bytes"
	"math/rand"
	"testing"
)

var testKeys = [][]byte{
	[]byte("k"),
	[]byte("phobos"),
	[]byte("Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI="),
	bytes.Repeat([]byte{0xFE}, 200),
}

var wireLengths = []int{32, 64, 92, 148, 176, 1024, 1420, 2000, 9000}

func wirePacket(rng *rand.Rand, length int) []byte {
	payload := make([]byte, length)
	rng.Read(payload)
	payload[0] = byte(1 + rng.Intn(4))
	payload[1], payload[2], payload[3] = 0, 0, 0
	return payload
}

func roundTrip(t *testing.T, key, payload []byte, obfuscateBytes int) {
	t.Helper()
	buf := make([]byte, 65535)
	copy(buf, payload)
	n := Encode(buf, len(payload), key, 4, obfuscateBytes)
	if n < 4 {
		t.Fatalf("encode returned %d: keylen=%d length=%d obf=%d",
			n, len(key), len(payload), obfuscateBytes)
	}
	got, _ := Decode(buf, n, key, obfuscateBytes)
	if got != len(payload) || !bytes.Equal(buf[:got], payload) {
		t.Fatalf("round trip corrupted the payload: keylen=%d length=%d obf=%d encoded=%d decoded=%d",
			len(key), len(payload), obfuscateBytes, n, got)
	}
}

// Covers the window where the encoder used to pick a different obfuscation
// span than the decoder: obfuscate-bytes at or above the payload length.
func TestEncodeDecodeRoundTrip(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	for _, key := range testKeys {
		for _, obfuscateBytes := range []int{0, 16, 64, 148, 200, 512} {
			for _, length := range wireLengths {
				for range 16 {
					roundTrip(t, key, wirePacket(rng, length), obfuscateBytes)
				}
			}
		}
	}
}

func TestStreamCipherRoundTrip(t *testing.T) {
	rng := rand.New(rand.NewSource(2))
	for _, key := range testKeys {
		for _, length := range []int{1, 64, 65, 1000, 20000} {
			payload := make([]byte, length)
			rng.Read(payload)

			buf := bytes.Clone(payload)
			split := length / 3
			enc := NewStreamCipher(key)
			enc.Apply(buf[:split])
			enc.Apply(buf[split:])
			if length > 4 && bytes.Equal(buf, payload) {
				t.Fatalf("stream cipher left the payload untouched at length %d", length)
			}

			dec := NewStreamCipher(key)
			dec.Apply(buf[:split])
			dec.Apply(buf[split:])
			if !bytes.Equal(buf, payload) {
				t.Fatalf("stream cipher round trip failed: keylen=%d length=%d", len(key), length)
			}
		}
	}
}

func keystream(key []byte, length int) []byte {
	buf := make([]byte, length)
	XOR(buf, length, key)
	return buf
}

// The C keystream cache is thread-local and was once keyed by packet length
// and key length alone, so a second key of the same length silently inherited
// the first key's keystream -- and the poison outlived the goroutine, because
// Go reuses OS threads. Two keys of equal length must produce different
// keystreams for the same packet length.
func TestKeysOfEqualLengthKeepSeparateKeystreams(t *testing.T) {
	keyA := []byte("aaaaaa")
	keyB := []byte("bbbbbb")
	if len(keyA) != len(keyB) {
		t.Fatal("the test needs two keys of equal length")
	}

	for _, length := range []int{32, 148, 1420, 1500} {
		a := keystream(keyA, length)
		b := keystream(keyB, length)
		if bytes.Equal(a, b) {
			t.Fatalf("keys of equal length share a keystream at length %d: %x", length, a[:8])
		}
	}

	rng := rand.New(rand.NewSource(3))
	for range 200 {
		for _, length := range []int{32, 148, 1420} {
			roundTrip(t, keyA, wirePacket(rng, length), 0)
			roundTrip(t, keyB, wirePacket(rng, length), 0)
		}
	}
}