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

package phobos

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"net/netip"
	"testing"

	"golang.zx2c4.com/wireguard/windows/phobos/cobf"
)

func digestOf(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func mustDecodeHex(t *testing.T, value string) []byte {
	t.Helper()
	data, err := hex.DecodeString(value)
	if err != nil {
		t.Fatalf("bad hex vector: %v", err)
	}
	return data
}

func TestMaskMatchesGolden(t *testing.T) {
	for _, vector := range maskVectors {
		buf := make([]byte, vector.length)
		NewObfuscator([]byte(vector.key)).xorData(buf)
		if got := digestOf(buf); got != vector.digest {
			t.Errorf("mask digest mismatch for key %q length %d: got %s want %s",
				vector.key, vector.length, got, vector.digest)
		}
	}
}

func TestMaskCacheStaysConsistent(t *testing.T) {
	obfuscator := NewObfuscator([]byte("Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI="))
	for _, vector := range maskVectors {
		if vector.key != "Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI=" {
			continue
		}
		for range 3 {
			buf := make([]byte, vector.length)
			obfuscator.xorData(buf)
			if got := digestOf(buf); got != vector.digest {
				t.Fatalf("cached mask drifted at length %d", vector.length)
			}
		}
	}
}

func TestStreamCipherMatchesGolden(t *testing.T) {
	for _, vector := range streamVectors {
		buf := make([]byte, vector.length)
		cipher := cobf.NewStreamCipher([]byte(vector.key))
		cipher.Apply(buf[:vector.split])
		cipher.Apply(buf[vector.split:])
		if got := digestOf(buf); got != vector.digest {
			t.Errorf("stream digest mismatch for key %q length %d split %d: got %s want %s",
				vector.key, vector.length, vector.split, got, vector.digest)
		}
	}
}

func TestDecodeMatchesGolden(t *testing.T) {
	for _, vector := range decodeVectors {
		encoded := mustDecodeHex(t, vector.encoded)
		want := mustDecodeHex(t, vector.decoded)
		buf := bytes.Clone(encoded)
		n := NewObfuscator([]byte(vector.key)).Decode(buf, len(buf), vector.obfuscateBytes)
		if n < 0 || !bytes.Equal(buf[:n], want) {
			t.Errorf("decode mismatch for key %q obfuscateBytes %d", vector.key, vector.obfuscateBytes)
		}
	}
}

func TestEncodeIsDecodable(t *testing.T) {
	for _, vector := range decodeVectors {
		payload := mustDecodeHex(t, vector.decoded)
		buf := make([]byte, BufferSize)
		copy(buf, payload)
		n := NewObfuscator([]byte(vector.key)).Encode(buf, len(payload), DefaultMaxDummy, vector.obfuscateBytes)
		if n < 0 {
			t.Fatalf("encode failed for key %q", vector.key)
		}
		n = NewObfuscator([]byte(vector.key)).Decode(buf[:n], n, vector.obfuscateBytes)
		if n < 0 || !bytes.Equal(buf[:n], payload) {
			t.Errorf("round trip failed for key %q obfuscateBytes %d", vector.key, vector.obfuscateBytes)
		}
	}
}

func TestBindingSuccessMatchesGolden(t *testing.T) {
	for _, vector := range bindingSuccessVectors {
		txid := mustDecodeHex(t, vector.txid)
		want := mustDecodeHex(t, vector.out)
		buf := make([]byte, 128)
		n := stunBuildBindingSuccess(buf, txid, netip.MustParseAddrPort(vector.addr))
		if n < 0 || !bytes.Equal(buf[:n], want) {
			t.Errorf("binding success mismatch for %s", vector.addr)
		}
	}
}

func TestStunFramingRoundTrip(t *testing.T) {
	var rng rng32 = 7
	for _, length := range []int{1, 4, 148, 1420, BufferSize - stunDataIndHeaderSize} {
		payload := make([]byte, length)
		for i := range payload {
			payload[i] = byte(i)
		}
		buf := make([]byte, BufferSize)
		copy(buf, payload)
		n := stunWrapDataIndication(buf, length, &rng)
		if n < 0 {
			t.Fatalf("wrap failed at length %d", length)
		}
		if !stunHasMagic(buf[:n]) || stunMessageType(buf) != stunDataIndication {
			t.Fatalf("wrapped frame is not a STUN data indication at length %d", length)
		}
		if unwrapped := stunUnwrapDataIndication(buf, n); unwrapped != length || !bytes.Equal(buf[:length], payload) {
			t.Fatalf("unwrap failed at length %d", length)
		}
	}
}

func TestStunWrapRejectsOverflow(t *testing.T) {
	var rng rng32 = 7
	buf := make([]byte, 64)
	if stunWrapDataIndication(buf, 60, &rng) >= 0 {
		t.Fatal("expected overflow rejection")
	}
}

func TestMediaFramingRoundTrip(t *testing.T) {
	params := MediaParams{PayloadType: 102, SSRC: 0xDEADBEEF, TimestampStep: 3000}
	masker := NewMasker(MaskingMEDIA, params)
	for _, length := range []int{4, 148, 1420} {
		payload := make([]byte, length)
		for i := range payload {
			payload[i] = byte(i * 7)
		}
		buf := make([]byte, BufferSize)
		copy(buf, payload)
		n := masker.OnDataWrap(buf, length)
		if n != length+rtpHeaderSize {
			t.Fatalf("unexpected wrapped length %d", n)
		}
		unwrapped := NewMasker(MaskingMEDIA, params).OnDataUnwrap(buf, n, netip.AddrPort{}, nil)
		if unwrapped != length || !bytes.Equal(buf[:length], payload) {
			t.Fatalf("media round trip failed at length %d", length)
		}
	}
}

func TestMediaSequenceAdvances(t *testing.T) {
	masker := NewMasker(MaskingMEDIA, MediaParams{PayloadType: 96, SSRC: 1, TimestampStep: 3000})
	buf := make([]byte, BufferSize)
	masker.OnDataWrap(buf, 64)
	first := [12]byte(buf[:12])
	masker.OnDataWrap(buf, 64)
	second := [12]byte(buf[:12])

	if first[0] != 0x80 || first[1] != 0x80|96 {
		t.Fatalf("unexpected RTP header start %x", first[:2])
	}
	if bytes.Equal(first[2:4], second[2:4]) {
		t.Fatal("RTP sequence number did not advance")
	}
	if bytes.Equal(first[4:8], second[4:8]) {
		t.Fatal("RTP timestamp did not advance")
	}
	if !bytes.Equal(first[8:12], second[8:12]) {
		t.Fatal("RTP SSRC must stay stable across frames")
	}
}

func TestParseMasking(t *testing.T) {
	for value, want := range map[string]Masking{
		"":      MaskingNone,
		"none":  MaskingNone,
		"NONE":  MaskingNone,
		" stun": MaskingSTUN,
		"MEDIA": MaskingMEDIA,
		"tls":   MaskingTLS,
	} {
		got, ok := ParseMasking(value)
		if !ok || got != want {
			t.Errorf("ParseMasking(%q) = %v, %v", value, got, ok)
		}
	}
	if _, ok := ParseMasking("quic"); ok {
		t.Error("expected unknown masking to be rejected")
	}
}