diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index d7f23a29..a8cc0649 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -61,6 +61,8 @@ proxy: # Proxy IP address/domain name and port. address: "127.0.0.1:1080" # Proxy authentication (optional). Put MTProxy secret in password field. + # For mtproxy, the secret must be hex-encoded (the same form mtg/MTProxy + # tools print, e.g. "ee" + 16-byte secret + cloak domain hex for faketls). username: password: diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index 2ee361c5..3f6fde76 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -17,13 +17,31 @@ package connector import ( + "encoding/hex" "fmt" + "strings" "golang.org/x/net/proxy" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs" ) +// decodeMTProxySecret parses an MTProxy secret string into raw bytes. +// MTProxy secrets are binary (faketls secrets begin with 0xEE, secured with 0xDD) +// and cannot be carried verbatim in a YAML string field, so we accept the standard +// hex encoding (optionally prefixed with "ee"/"dd") used by mtg/MTProxy tooling. +func decodeMTProxySecret(s string) ([]byte, error) { + s = strings.TrimSpace(s) + if s == "" { + return nil, fmt.Errorf("mtproxy secret is empty") + } + b, err := hex.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("mtproxy secret must be hex-encoded: %w", err) + } + return b, nil +} + func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) { switch cfg.Type { // we can't proxy HTTP through mtproxy @@ -54,7 +72,11 @@ func GetProxyResolver(cfg ProxyConfig) (dcs.Resolver, error) { resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer}) return resolver, nil case "mtproxy": - return dcs.MTProxy(cfg.Address, []byte(cfg.Password), dcs.MTProxyOptions{}) + secret, err := decodeMTProxySecret(cfg.Password) + if err != nil { + return nil, err + } + return dcs.MTProxy(cfg.Address, secret, dcs.MTProxyOptions{}) default: return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type) } diff --git a/pkg/connector/proxy_test.go b/pkg/connector/proxy_test.go new file mode 100644 index 00000000..d6f3113c --- /dev/null +++ b/pkg/connector/proxy_test.go @@ -0,0 +1,37 @@ +package connector + +import ( + "bytes" + "testing" +) + +func TestDecodeMTProxySecret(t *testing.T) { + // faketls secret: 0xee + 16 bytes + cloak domain ("working-name.ru" = 15 bytes) + hexSecret := "ee971746d927f4c0138b18447bfe1269bc70312e776f726b696e672d6e616d652e7275" + want := []byte{ + 0xee, + 0x97, 0x17, 0x46, 0xd9, 0x27, 0xf4, 0xc0, 0x13, + 0x8b, 0x18, 0x44, 0x7b, 0xfe, 0x12, 0x69, 0xbc, + 0x70, 0x31, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x69, + 0x6e, 0x67, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x2e, + 0x72, 0x75, + } + got, err := decodeMTProxySecret(hexSecret) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(got, want) { + t.Fatalf("decoded bytes mismatch:\n got=%x\nwant=%x", got, want) + } + + if _, err := decodeMTProxySecret(" " + hexSecret + "\n"); err != nil { + t.Fatalf("whitespace should be tolerated: %v", err) + } + + if _, err := decodeMTProxySecret(""); err == nil { + t.Fatal("expected error for empty secret") + } + if _, err := decodeMTProxySecret("not-hex!!"); err == nil { + t.Fatal("expected error for non-hex secret") + } +} diff --git a/pkg/gotd/mtproxy/faketls/ccs_test.go b/pkg/gotd/mtproxy/faketls/ccs_test.go new file mode 100644 index 00000000..07b7e990 --- /dev/null +++ b/pkg/gotd/mtproxy/faketls/ccs_test.go @@ -0,0 +1,65 @@ +package faketls + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestReadSkipsChangeCipherSpec ensures interleaved ChangeCipherSpec records +// do not pollute the Application-data stream. Earlier behaviour wrote the +// 1-byte CCS payload into readBuf, which desynced the obfuscated2 CTR +// keystream and produced "msg_key is invalid" on decrypted MTProto messages. +func TestReadSkipsChangeCipherSpec(t *testing.T) { + a := require.New(t) + + wire := bytes.NewBuffer(nil) + + // CCS record (1 byte = 0x01) + _, err := writeRecord(wire, record{ + Type: RecordTypeChangeCipherSpec, + Version: Version12Bytes, + Data: []byte{0x01}, + }) + a.NoError(err) + + // Application record carrying our payload + payload := []byte("hello-mtproto-bytes") + _, err = writeRecord(wire, record{ + Type: RecordTypeApplication, + Version: Version12Bytes, + Data: payload, + }) + a.NoError(err) + + // Another CCS in the middle + _, err = writeRecord(wire, record{ + Type: RecordTypeChangeCipherSpec, + Version: Version12Bytes, + Data: []byte{0x01}, + }) + a.NoError(err) + + // Second application record + more := []byte("second-payload") + _, err = writeRecord(wire, record{ + Type: RecordTypeApplication, + Version: Version12Bytes, + Data: more, + }) + a.NoError(err) + + tls := NewFakeTLS(zeroReader{}, &readonly{r: wire}) + + got, err := io.ReadAll(io.LimitReader(tls, int64(len(payload)+len(more)))) + a.NoError(err) + a.Equal(append(append([]byte(nil), payload...), more...), got) +} + +// readonly adapts an io.Reader to io.ReadWriter (NewFakeTLS demands one). +type readonly struct{ r io.Reader } + +func (r *readonly) Read(p []byte) (int, error) { return r.r.Read(p) } +func (r *readonly) Write(p []byte) (int, error) { return len(p), nil } diff --git a/pkg/gotd/mtproxy/faketls/client_hello.go b/pkg/gotd/mtproxy/faketls/client_hello.go index 4efe5ba8..7d51489b 100644 --- a/pkg/gotd/mtproxy/faketls/client_hello.go +++ b/pkg/gotd/mtproxy/faketls/client_hello.go @@ -14,7 +14,26 @@ import ( const clientHelloLength = 517 -func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte) (randomOffset int) { +// generateGrease produces seven GREASE bytes following the TLS spec +// (RFC 8701) and tdlib's TlsInit.cpp constraints used by MTProxy faketls +// validators: each byte has the form 0x?A (low nibble 0x0A), and grease[3] +// must differ from grease[4]. +func generateGrease(rng io.Reader) ([7]byte, error) { + var raw [7]byte + if _, err := io.ReadFull(rng, raw[:]); err != nil { + return raw, errors.Wrap(err, "read grease entropy") + } + var g [7]byte + for i, r := range raw { + g[i] = (r & 0xF0) | 0x0A + } + if g[3] == g[4] { + g[3] ^= 0x10 + } + return g, nil +} + +func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte, grease [7]byte) (randomOffset int) { S := func(s string) { b.Buf = append(b.Buf, s...) } @@ -22,8 +41,9 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32 randomOffset = len(b.Buf) b.Expand(n) } - G := func(_ int) { - b.Expand(0) + G := func(n int) { + v := grease[n] + b.Buf = append(b.Buf, v, v) } R := func() { b.Buf = append(b.Buf, sessionID[:]...) @@ -83,9 +103,18 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32 G(3) S("\x00\x01\x00\x00\x15") - if pad := clientHelloLength - b.Len(); pad > 0 { - b.Expand(pad) + // Padding extension (id 0x0015 already written above): write its + // length so the resulting ClientHello is exactly clientHelloLength + // bytes, then fill the body with zeros. + padLen := clientHelloLength - b.Len() - 2 + if padLen < 0 { + padLen = 0 } + lenPos := b.Len() + b.Expand(2) + binary.BigEndian.PutUint16(b.Buf[lenPos:lenPos+2], uint16(padLen)) + b.Expand(padLen) + return randomOffset } @@ -94,15 +123,21 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32 // See https://tools.ietf.org/html/rfc5246#section-7.4.1.1. func writeClientHello( w io.Writer, + rng io.Reader, now clock.Clock, sessionID [32]byte, domain string, secret []byte, ) (r [32]byte, err error) { + grease, err := generateGrease(rng) + if err != nil { + return [32]byte{}, err + } + b := &bin.Buffer{ Buf: make([]byte, 0, 576), } - randomOffset := createClientHello(b, sessionID, domain, [32]byte{}) + randomOffset := createClientHello(b, sessionID, domain, [32]byte{}, grease) // https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384 mac := hmac.New(sha256.New, secret) diff --git a/pkg/gotd/mtproxy/faketls/faketls.go b/pkg/gotd/mtproxy/faketls/faketls.go index 2833f4de..d8bd5c59 100644 --- a/pkg/gotd/mtproxy/faketls/faketls.go +++ b/pkg/gotd/mtproxy/faketls/faketls.go @@ -46,7 +46,7 @@ func (o *FakeTLS) Handshake(protocol [4]byte, dc int, s mtproxy.Secret) error { return errors.Wrap(err, "generate sessionID") } - clientDigest, err := writeClientHello(o.conn, o.clock, sessionID, s.CloakHost, s.Secret) + clientDigest, err := writeClientHello(o.conn, o.rand, o.clock, sessionID, s.CloakHost, s.Secret) if err != nil { return errors.Wrap(err, "send ClientHello") } @@ -93,20 +93,27 @@ func (o *FakeTLS) Read(b []byte) (n int, err error) { return o.readBuf.Read(b) } - rec, err := readRecord(o.conn) - if err != nil { - return 0, errors.Wrap(err, "read TLS record") - } + // Skip ChangeCipherSpec records — they are TLS-level keep-alive / + // compatibility markers (one-byte payload 0x01) and must NOT be + // passed up to the obfuscated2 layer, otherwise the CTR keystream + // position desyncs and subsequent MTProto messages decrypt to + // garbage (`msg_key is invalid`). + for { + rec, err := readRecord(o.conn) + if err != nil { + return 0, errors.Wrap(err, "read TLS record") + } - switch rec.Type { - case RecordTypeChangeCipherSpec: - case RecordTypeApplication: - case RecordTypeHandshake: - return 0, errors.New("unexpected record type handshake") - default: - return 0, errors.Errorf("unsupported record type %v", rec.Type) + switch rec.Type { + case RecordTypeChangeCipherSpec: + continue + case RecordTypeApplication: + o.readBuf.Write(rec.Data) + return o.readBuf.Read(b) + case RecordTypeHandshake: + return 0, errors.New("unexpected record type handshake") + default: + return 0, errors.Errorf("unsupported record type %v", rec.Type) + } } - o.readBuf.Write(rec.Data) - - return o.readBuf.Read(b) } diff --git a/pkg/gotd/mtproxy/faketls/faketls_test.go b/pkg/gotd/mtproxy/faketls/faketls_test.go index 5cee1e5a..9d55f639 100644 --- a/pkg/gotd/mtproxy/faketls/faketls_test.go +++ b/pkg/gotd/mtproxy/faketls/faketls_test.go @@ -10,6 +10,15 @@ import ( "github.com/gotd/neo" ) +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + func TestTLS(t *testing.T) { a := require.New(t) secret := [32]byte{} @@ -17,42 +26,42 @@ func TestTLS(t *testing.T) { c := neo.NewTime(time.Date(2010, 10, 10, 1, 1, 1, 0, time.UTC)) b := bytes.NewBuffer(nil) - _, err := writeClientHello(b, c, sessionID, "google.com", secret[:]) + _, err := writeClientHello(b, zeroReader{}, c, sessionID, "google.com", secret[:]) a.NoError(err) testVector := []byte{ - 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xf9, 0x75, 0x5f, 0xdd, 0xb9, - 0xe3, 0x46, 0x57, 0x5a, 0x26, 0x71, 0xfa, 0x29, 0x7f, 0xab, 0xf0, 0xa1, 0xf3, 0x69, 0x4f, 0x72, - 0xe0, 0xc3, 0x8f, 0x62, 0x77, 0x5c, 0x8f, 0x5a, 0xf8, 0xa2, 0xa9, 0x20, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13, 0x01, - 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8, - 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, - 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, - 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, - 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, - 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x00, - 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, - 0x0a, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, - 0x00, 0x01, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xa9, 0xa8, 0x7f, 0x37, 0x9b, + 0x09, 0x80, 0x6a, 0xf3, 0xff, 0x78, 0x4a, 0x6c, 0x4e, 0xbd, 0xdd, 0x94, 0x31, 0x8e, 0x7c, 0x09, + 0x36, 0x63, 0x77, 0x1d, 0x36, 0xf4, 0xcb, 0x6d, 0x3e, 0x13, 0x83, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a, 0x0a, + 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, + 0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, + 0x01, 0x93, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x0a, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, + 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, + 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, + 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x2b, 0x00, 0x29, 0x0a, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, + 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x03, 0x03, + 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 0x1a, 0x1a, 0x00, 0x01, 0x00, 0x00, + 0x15, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } a.Equal(testVector, b.Bytes()) diff --git a/pkg/gotd/mtproxy/faketls/server_hello.go b/pkg/gotd/mtproxy/faketls/server_hello.go index ff148dad..abda1f48 100644 --- a/pkg/gotd/mtproxy/faketls/server_hello.go +++ b/pkg/gotd/mtproxy/faketls/server_hello.go @@ -4,11 +4,20 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "encoding/hex" "io" "github.com/go-faster/errors" ) +// peekDump returns up to n bytes from the start of buf as a hex string for diagnostics. +func peekDump(buf []byte, n int) string { + if len(buf) < n { + n = len(buf) + } + return hex.EncodeToString(buf[:n]) +} + // readServerHello reads faketls ServerHello. func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error { packetBuf := bytes.NewBuffer(nil) @@ -16,10 +25,11 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error { handshake, err := readRecord(r) if err != nil { - return errors.Wrap(err, "handshake record") + return errors.Wrapf(err, "handshake record (peek=%s)", peekDump(packetBuf.Bytes(), 32)) } if handshake.Type != RecordTypeHandshake { - return errors.Wrap(err, "unexpected record type") + return errors.Errorf("unexpected handshake record type: got 0x%02x, want 0x%02x (peek=%s)", + byte(handshake.Type), byte(RecordTypeHandshake), peekDump(packetBuf.Bytes(), 32)) } changeCipher, err := readRecord(r) @@ -27,7 +37,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error { return errors.Wrap(err, "change cipher record") } if changeCipher.Type != RecordTypeChangeCipherSpec { - return errors.Wrap(err, "unexpected record type") + return errors.Errorf("unexpected change cipher record type: got 0x%02x, want 0x%02x", + byte(changeCipher.Type), byte(RecordTypeChangeCipherSpec)) } cert, err := readRecord(r) @@ -35,7 +46,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error { return errors.Wrap(err, "cert record") } if cert.Type != RecordTypeApplication { - return errors.Wrap(err, "unexpected record type") + return errors.Errorf("unexpected application record type: got 0x%02x, want 0x%02x", + byte(cert.Type), byte(RecordTypeApplication)) } // `$record_header = type 1 byte + version 2 bytes + payload_length 2 bytes = 5 bytes` diff --git a/pkg/gotd/mtproxy/faketls/structure_test.go b/pkg/gotd/mtproxy/faketls/structure_test.go new file mode 100644 index 00000000..7f0e643f --- /dev/null +++ b/pkg/gotd/mtproxy/faketls/structure_test.go @@ -0,0 +1,101 @@ +package faketls + +import ( + "crypto/tls" + "net" + "strings" + "testing" + "time" + + "go.mau.fi/mautrix-telegram/pkg/gotd/bin" +) + +// TestClientHelloStructure verifies that what we generate is a syntactically +// valid TLS ClientHello — i.e., the Go crypto/tls server can parse it without +// returning a "decode_error"-like syntax error. We don't care that the TLS +// handshake then fails (it will, since we're using a fake cert / random data); +// we only care that parsing succeeds. +func TestClientHelloStructure(t *testing.T) { + // Render with deterministic rng + sessionID + key. + var session [32]byte + for i := range session { + session[i] = byte(i) + } + var key [32]byte + for i := range key { + key[i] = 0xAA + } + var grease [7]byte + for i := range grease { + grease[i] = byte(0x0A + i*0x10) + } + if grease[3] == grease[4] { + grease[3] ^= 0x10 + } + + b := &bin.Buffer{Buf: make([]byte, 0, 576)} + createClientHello(b, session, "example.com", key, grease) + if len(b.Buf) != clientHelloLength { + t.Fatalf("expected %d bytes, got %d", clientHelloLength, len(b.Buf)) + } + + // Wire it through a real TLS server. The server reads bytes from + // our pipe; if it accepts ClientHello but fails on cert/MAC, we get + // a non-syntax error. If it returns "decode_error", we know we're + // still busted. + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + defer serverConn.Close() + + go func() { + clientConn.Write(b.Buf) + // keep the pipe open until done + }() + + cfg := &tls.Config{ + Certificates: []tls.Certificate{generateSelfSigned(t)}, + } + srv := tls.Server(serverConn, cfg) + srv.SetDeadline(time.Now().Add(2 * time.Second)) + err := srv.Handshake() + if err == nil { + return // unexpectedly succeeded — fine for our purpose + } + t.Logf("server handshake error (expected non-syntax): %v", err) + + msg := err.Error() + for _, marker := range []string{"decode_error", "syntax", "malformed", "bad ClientHello"} { + if strings.Contains(msg, marker) { + t.Fatalf("structural parse failure (%q) — ClientHello is malformed: %v", marker, err) + } + } +} + +// generateSelfSigned builds a throwaway cert for the test TLS server. +func generateSelfSigned(t *testing.T) tls.Certificate { + cert, err := tls.X509KeyPair(testCertPEM, testKeyPEM) + if err != nil { + t.Fatal(err) + } + return cert +} + +// Generated with `go run filippo.io/mkcert@latest -ecdsa example.com`-ish. +// Embedded here for deterministic test environment. +var testCertPEM = []byte(`-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow +EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d +7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B +5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr +BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 +NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l +Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc +6MF9+Yw1Yy0t +-----END CERTIFICATE-----`) + +var testKeyPEM = []byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 +AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q +EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== +-----END EC PRIVATE KEY-----`) diff --git a/pkg/gotd/mtproxy/obfuscated2/obfuscated2.go b/pkg/gotd/mtproxy/obfuscated2/obfuscated2.go index fe642d1f..7aecae38 100644 --- a/pkg/gotd/mtproxy/obfuscated2/obfuscated2.go +++ b/pkg/gotd/mtproxy/obfuscated2/obfuscated2.go @@ -53,7 +53,12 @@ func (o *Obfuscated2) Read(b []byte) (int, error) { return n, err } if n > 0 { - o.decrypt.XORKeyStream(b, b) + // IMPORTANT: only XOR the n bytes that were actually read. + // XOR-ing the full b advances the CTR keystream past where the + // server is and permanently desyncs the stream — every later + // MTProto message decrypts to garbage and the engine fails + // with "msg_key is invalid". + o.decrypt.XORKeyStream(b[:n], b[:n]) } return n, err } diff --git a/pkg/gotd/mtproxy/obfuscated2/short_read_test.go b/pkg/gotd/mtproxy/obfuscated2/short_read_test.go new file mode 100644 index 00000000..bb2b32c8 --- /dev/null +++ b/pkg/gotd/mtproxy/obfuscated2/short_read_test.go @@ -0,0 +1,82 @@ +package obfuscated2 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "io" + "testing" + + "github.com/stretchr/testify/require" +) + +// chunkConn delivers data from buf in chunks of at most chunkSize bytes. +type chunkConn struct { + buf *bytes.Buffer + chunkSize int +} + +func (c *chunkConn) Read(p []byte) (int, error) { + if c.buf.Len() == 0 { + return 0, io.EOF + } + want := len(p) + if want > c.chunkSize { + want = c.chunkSize + } + return c.buf.Read(p[:want]) +} + +func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil } + +// TestShortReadKeepsKeystreamAligned ensures that when the underlying +// transport returns fewer bytes than the caller asked for, the CTR +// keystream is only advanced by the bytes actually delivered. +// +// The previous implementation called XORKeyStream(b, b) instead of +// XORKeyStream(b[:n], b[:n]); after a single short read the client and +// server keystreams diverged and every subsequent MTProto message +// failed integrity (msg_key invalid). +func TestShortReadKeepsKeystreamAligned(t *testing.T) { + a := require.New(t) + + key := bytes.Repeat([]byte{0x11}, 32) + iv := bytes.Repeat([]byte{0x22}, 16) + + enc, err := aes.NewCipher(key) + a.NoError(err) + dec, err := aes.NewCipher(key) + a.NoError(err) + + encStream := cipher.NewCTR(enc, iv) + decStream := cipher.NewCTR(dec, iv) + + plaintext := bytes.Repeat([]byte("Hello, MTProxy! "), 50) + ciphertext := make([]byte, len(plaintext)) + encStream.XORKeyStream(ciphertext, plaintext) + + wire := &chunkConn{buf: bytes.NewBuffer(append([]byte(nil), ciphertext...)), chunkSize: 7} + o := &Obfuscated2{ + conn: wire, + keys: keys{decrypt: decStream}, + } + + got := make([]byte, len(plaintext)) + off := 0 + for off < len(plaintext) { + end := off + 128 + if end > len(got) { + end = len(got) + } + n, err := o.Read(got[off:end]) + if err != nil && err != io.EOF { + t.Fatalf("read at off %d: %v", off, err) + } + if n == 0 { + t.Fatalf("zero-length read at off %d", off) + } + off += n + } + + a.Equal(plaintext, got, "short reads must not desync the keystream") +}