initial commit

This commit is contained in:
iceBear67
2026-07-15 14:28:58 +08:00
commit 6e0d7ec33f
47 changed files with 4022 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
package wire
import (
"crypto/sha3"
"encoding/hex"
"golang.org/x/crypto/chacha20"
)
// Direction labels for per-direction key derivation (PROTOCOL.md §3).
const (
DirC2S byte = 0x01 // client -> server
DirS2C byte = 0x02 // server -> client
)
// SHA3_224 returns the SHA3-224 digest of in.
func SHA3_224(in []byte) []byte {
h := sha3.New224()
h.Write(in)
return h.Sum(nil)
}
// PSKAddress is the Handshake Server Address for Intent 17: hex(SHA3-224(PSK)).
func PSKAddress(psk []byte) string {
return hex.EncodeToString(SHA3_224(psk))
}
// DeriveKey computes the 32-byte ChaCha20 key: SHA3-256(phaseKey || dir).
func DeriveKey(phaseKey []byte, dir byte) []byte {
h := sha3.New256()
h.Write(phaseKey)
h.Write([]byte{dir})
return h.Sum(nil)
}
// CipherFor builds a ChaCha20 stream cipher for the given phase key and
// direction. ChaCha20 is symmetric, so the same cipher encrypts and decrypts.
func CipherFor(phaseKey []byte, dir byte) *chacha20.Cipher {
c, err := chacha20.NewUnauthenticatedCipher(DeriveKey(phaseKey, dir), make([]byte, 12))
if err != nil {
panic("wire: chacha20 init: " + err.Error())
}
return c
}
+78
View File
@@ -0,0 +1,78 @@
package wire
import (
"bufio"
"errors"
"io"
"net"
"sync"
"golang.org/x/crypto/chacha20"
)
// MaxFrame is the maximum decrypted frame payload size (1 MiB).
const MaxFrame = 1 << 20
var errFrameTooBig = errors.New("wire: frame exceeds max size")
// FramedConn is the encrypted, length-prefixed frame transport (PROTOCOL.md §3.1).
// The VarInt length prefix is plaintext; the payload is ChaCha20-encrypted with a
// continuous per-direction keystream. Writes are serialized; reads are expected
// from a single goroutine.
type FramedConn struct {
conn net.Conn
r *bufio.Reader
in *chacha20.Cipher
out *chacha20.Cipher
wmu sync.Mutex
}
func NewFramedConn(conn net.Conn, in, out *chacha20.Cipher) *FramedConn {
return &FramedConn{
conn: conn,
r: bufio.NewReader(conn),
in: in,
out: out,
}
}
// SwitchCiphers swaps both ciphers at a frame boundary (Phase A → Phase B).
// Only call this from the same goroutine sequence as reads/writes during the
// handshake, before concurrency begins.
func (f *FramedConn) SwitchCiphers(in, out *chacha20.Cipher) {
f.in = in
f.out = out
}
// ReadFrame reads and decrypts one frame payload.
func (f *FramedConn) ReadFrame() ([]byte, error) {
n, err := ReadVarInt(f.r)
if err != nil {
return nil, err
}
if n < 0 || n > MaxFrame {
return nil, errFrameTooBig
}
ct := make([]byte, n)
if _, err := io.ReadFull(f.r, ct); err != nil {
return nil, err
}
f.in.XORKeyStream(ct, ct) // decrypt in place
return ct, nil
}
// WriteFrame encrypts and sends one frame payload. Safe for concurrent callers.
func (f *FramedConn) WriteFrame(payload []byte) error {
f.wmu.Lock()
defer f.wmu.Unlock()
ct := make([]byte, len(payload))
f.out.XORKeyStream(ct, payload)
out := AppendVarInt(make([]byte, 0, VarIntMaxBytes+len(ct)), len(ct))
out = append(out, ct...)
_, err := f.conn.Write(out)
return err
}
func (f *FramedConn) Close() error { return f.conn.Close() }
func (f *FramedConn) RemoteAddr() net.Addr { return f.conn.RemoteAddr() }
+122
View File
@@ -0,0 +1,122 @@
package wire
import (
"bytes"
"encoding/binary"
"errors"
)
// Writer builds redapricot/Minecraft primitive types into a byte slice.
type Writer struct {
buf []byte
}
func NewWriter() *Writer { return &Writer{} }
func (w *Writer) U8(v byte) *Writer { w.buf = append(w.buf, v); return w }
func (w *Writer) VarInt(v int) *Writer { w.buf = AppendVarInt(w.buf, v); return w }
func (w *Writer) U16(v uint16) *Writer { w.buf = append(w.buf, byte(v>>8), byte(v)); return w }
func (w *Writer) Bytes(p []byte) *Writer { w.buf = append(w.buf, p...); return w }
func (w *Writer) I64(v int64) *Writer {
var b [8]byte
binary.BigEndian.PutUint64(b[:], uint64(v))
w.buf = append(w.buf, b[:]...)
return w
}
func (w *Writer) String(s string) *Writer {
w.VarInt(len(s))
w.buf = append(w.buf, s...)
return w
}
// Out returns the built bytes.
func (w *Writer) Out() []byte { return w.buf }
// Reader consumes redapricot/Minecraft primitive types from a byte slice.
type Reader struct {
buf []byte
pos int
}
func NewReader(b []byte) *Reader { return &Reader{buf: b} }
var errUnderflow = errors.New("wire: read underflow")
func (r *Reader) U8() (byte, error) {
if r.pos >= len(r.buf) {
return 0, errUnderflow
}
v := r.buf[r.pos]
r.pos++
return v, nil
}
func (r *Reader) VarInt() (int, error) {
br := bytes.NewReader(r.buf[r.pos:])
before := br.Len()
v, err := ReadVarInt(br)
if err != nil {
return 0, err
}
r.pos += before - br.Len()
return v, nil
}
func (r *Reader) U16() (uint16, error) {
if r.pos+2 > len(r.buf) {
return 0, errUnderflow
}
v := binary.BigEndian.Uint16(r.buf[r.pos:])
r.pos += 2
return v, nil
}
func (r *Reader) I64() (int64, error) {
if r.pos+8 > len(r.buf) {
return 0, errUnderflow
}
v := int64(binary.BigEndian.Uint64(r.buf[r.pos:]))
r.pos += 8
return v, nil
}
func (r *Reader) Bytes(n int) ([]byte, error) {
if n < 0 || r.pos+n > len(r.buf) {
return nil, errUnderflow
}
out := make([]byte, n)
copy(out, r.buf[r.pos:r.pos+n])
r.pos += n
return out, nil
}
func (r *Reader) String() (string, error) {
n, err := r.VarInt()
if err != nil {
return "", err
}
b, err := r.Bytes(n)
if err != nil {
return "", err
}
return string(b), nil
}
// Remaining returns the unread bytes (a copy is not made).
func (r *Reader) Remaining() []byte { return r.buf[r.pos:] }
// BuildHandshake produces a full uncompressed Minecraft Handshake packet
// (length-prefixed, packet id 0x00).
func BuildHandshake(protocolVersion int, address string, port uint16, intent int) []byte {
body := NewWriter().
VarInt(0x00). // packet id
VarInt(protocolVersion).
String(address).
U16(port).
VarInt(intent).
Out()
out := AppendVarInt(nil, len(body))
return append(out, body...)
}
+51
View File
@@ -0,0 +1,51 @@
package wire
import (
"errors"
"io"
)
// VarIntMaxBytes is the maximum encoded length of a Minecraft VarInt.
const VarIntMaxBytes = 5
var errVarIntTooBig = errors.New("wire: VarInt exceeds 5 bytes")
// ReadVarInt reads a Minecraft-style VarInt from a byte reader.
func ReadVarInt(r io.ByteReader) (int, error) {
var value, shift int
for {
b, err := r.ReadByte()
if err != nil {
return 0, err
}
value |= int(b&0x7F) << shift
if b&0x80 == 0 {
return value, nil
}
shift += 7
if shift >= 32 {
return 0, errVarIntTooBig
}
}
}
// AppendVarInt appends v encoded as a VarInt to dst.
func AppendVarInt(dst []byte, v int) []byte {
u := uint32(v)
for u&^uint32(0x7F) != 0 {
dst = append(dst, byte(u&0x7F)|0x80)
u >>= 7
}
return append(dst, byte(u))
}
// VarIntSize returns the encoded byte length of v.
func VarIntSize(v int) int {
n := 1
u := uint32(v)
for u&^uint32(0x7F) != 0 {
u >>= 7
n++
}
return n
}
+105
View File
@@ -0,0 +1,105 @@
package wire
import (
"bytes"
"net"
"sync"
"testing"
)
// newFramedPair returns two FramedConns wired over an in-memory pipe with
// matching per-direction ciphers (client out=C2S/in=S2C, server the reverse).
func newFramedPair(key []byte) (client, server *FramedConn) {
c, s := net.Pipe()
client = NewFramedConn(c, CipherFor(key, DirS2C), CipherFor(key, DirC2S))
server = NewFramedConn(s, CipherFor(key, DirC2S), CipherFor(key, DirS2C))
return client, server
}
func TestVarIntRoundTrip(t *testing.T) {
cases := []int{0, 1, 127, 128, 255, 300, 16384, 2097151, 1 << 30}
for _, v := range cases {
enc := AppendVarInt(nil, v)
if len(enc) != VarIntSize(v) {
t.Fatalf("size mismatch for %d: got %d want %d", v, len(enc), VarIntSize(v))
}
got, err := ReadVarInt(bytes.NewReader(enc))
if err != nil {
t.Fatalf("read %d: %v", v, err)
}
if got != v {
t.Fatalf("roundtrip %d -> %d", v, got)
}
}
}
// TestPSKAddress cross-validates SHA3-224 against the value the Java hub prints
// for the PSK "test-psk" (locks the two implementations together).
func TestPSKAddress(t *testing.T) {
const want = "90188f2d84e273e4d6fb27194b4a88ad10bcc20de00c493beae6d18f"
if got := PSKAddress([]byte("test-psk")); got != want {
t.Fatalf("PSKAddress = %s, want %s", got, want)
}
}
// TestFramedConnRoundTrip exercises the encrypted framing + keystream continuity
// in both directions over an in-memory pipe.
func TestFramedConnRoundTrip(t *testing.T) {
cli, srv := newFramedPair([]byte("unit-key"))
// Frames of varying sizes to exercise partial-block keystream state.
payloads := [][]byte{
[]byte("a"),
bytes.Repeat([]byte{0xAB}, 63),
bytes.Repeat([]byte{0xCD}, 64),
bytes.Repeat([]byte{0xEF}, 65),
bytes.Repeat([]byte("mux"), 5000),
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for _, p := range payloads {
if err := cli.WriteFrame(p); err != nil {
t.Errorf("client write: %v", err)
return
}
}
}()
for _, want := range payloads {
got, err := srv.ReadFrame()
if err != nil {
t.Fatalf("server read: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("frame mismatch: len(got)=%d len(want)=%d", len(got), len(want))
}
}
wg.Wait()
// Reverse direction.
go func() {
_ = srv.WriteFrame([]byte("pong"))
}()
got, err := cli.ReadFrame()
if err != nil {
t.Fatalf("client read: %v", err)
}
if !bytes.Equal(got, []byte("pong")) {
t.Fatalf("reverse frame mismatch: %q", got)
}
}
func TestProxyBufferShapeIsStable(t *testing.T) {
// A frame with an empty payload must still be a valid (zero-length) frame.
cli, srv := newFramedPair([]byte("k"))
go func() { _ = cli.WriteFrame(nil) }()
got, err := srv.ReadFrame()
if err != nil {
t.Fatalf("read empty frame: %v", err)
}
if len(got) != 0 {
t.Fatalf("expected empty payload, got %d bytes", len(got))
}
}