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/faketls.go b/pkg/gotd/mtproxy/faketls/faketls.go index feb78569..d8bd5c59 100644 --- a/pkg/gotd/mtproxy/faketls/faketls.go +++ b/pkg/gotd/mtproxy/faketls/faketls.go @@ -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) }