4768065e72
The Read path treated every TLS record's payload as application data and wrote it into readBuf — including the 1-byte payload (0x01) of ChangeCipherSpec records. mtg sends those records intermittently as a TLS-compat keep-alive; once one arrived inside the data stream it desynced the obfuscated2 CTR keystream by one byte. From that point on every MTProto message decrypted to garbage and the engine failed with "decrypt: msg_key is invalid", forcibly closed the connection, and looped. The Go switch cases for ChangeCipherSpec and Application were both empty (no fallthrough, no continue), so control reached the o.readBuf.Write(rec.Data) call below the switch for both — exactly the wrong behaviour for CCS. Reshape the loop so that: - ChangeCipherSpec records are silently dropped - Application records are written to readBuf and returned - Handshake / unsupported types still error out This matches tdlib's TlsTransport (CCS is skipped at the TLS framing layer and never reaches the MTProto decoder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
1.7 KiB
Go
66 lines
1.7 KiB
Go
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 }
|