ccb349f3d2
Read called XORKeyStream(b, b) — XOR-ing the entire caller buffer even when the underlying transport returned fewer bytes. AES-CTR's keystream position is then advanced by len(b), but the peer only consumed n bytes' worth of keystream. After a single short read the two keystreams diverge for the lifetime of the connection, every subsequent MTProto message decrypts to garbage, and the engine fails with "consume message: decrypt: msg_key is invalid". The faketls layer makes short reads routine: each Read returns at most one TLS Application record's payload, regardless of how big the caller buffer is. So in practice the stream desynced almost immediately on high-traffic clients (active supergroups, post-relogin catch-up) and intermittently on quiet ones. Match the upstream gotd/td fix and only XOR the n bytes that came out of the transport. Add a regression test (chunkConn delivers ciphertext in 7-byte chunks; client reads through Obfuscated2.Read with a 128-byte buffer; plaintext must round-trip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
2.0 KiB
Go
83 lines
2.0 KiB
Go
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")
|
|
}
|