Repository: gotd/td
Pull request #556 (2021-11-11 13:13:51)

feat(tdesktop): add full gotd session mapping


Patch:
diff --git a/session/tdesktop.go b/session/tdesktop.go
index e84b24197..85ef4fd5e 100644
--- a/session/tdesktop.go
+++ b/session/tdesktop.go
@@ -25,9 +25,61 @@ func findDCAddr(list []tg.DCOption, dcID int) string {
 	return ""
 }
 
+func mapConfig(mainDC int, cfg tdesktop.MTPConfig) tg.Config {
+	var opts []tg.DCOption
+	for _, dc := range cfg.DCOptions.Options {
+		opts = append(opts, tg.DCOption{
+			Flags:             dc.Flags,
+			Ipv6:              dc.IPv6(),
+			MediaOnly:         dc.MediaOnly(),
+			TCPObfuscatedOnly: dc.TCPOOnly(),
+			CDN:               dc.CDN(),
+			Static:            dc.Static(),
+			ID:                int(dc.ID),
+			IPAddress:         dc.IP,
+			Port:              int(dc.Port),
+			Secret:            dc.Secret,
+		})
+	}
+	return tg.Config{
+		DCOptions:               opts,
+		ThisDC:                  mainDC,
+		ChatSizeMax:             int(cfg.ChatSizeMax),
+		MegagroupSizeMax:        int(cfg.MegagroupSizeMax),
+		ForwardedCountMax:       int(cfg.ForwardedCountMax),
+		OnlineUpdatePeriodMs:    int(cfg.OnlineUpdatePeriod),
+		OfflineBlurTimeoutMs:    int(cfg.OfflineBlurTimeout),
+		OfflineIdleTimeoutMs:    int(cfg.OfflineIdleTimeout),
+		OnlineCloudTimeoutMs:    int(cfg.OnlineCloudTimeout),
+		NotifyCloudDelayMs:      int(cfg.NotifyCloudDelay),
+		NotifyDefaultDelayMs:    int(cfg.NotifyDefaultDelay),
+		SavedGifsLimit:          int(cfg.SavedGifsLimit),
+		EditTimeLimit:           int(cfg.EditTimeLimit),
+		RevokeTimeLimit:         int(cfg.RevokeTimeLimit),
+		RevokePmTimeLimit:       int(cfg.RevokePrivateTimeLimit),
+		RevokePmInbox:           cfg.RevokePrivateInbox,
+		StickersRecentLimit:     int(cfg.StickersRecentLimit),
+		StickersFavedLimit:      int(cfg.StickersFavedLimit),
+		PinnedDialogsCountMax:   int(cfg.PinnedDialogsCountMax),
+		PinnedInfolderCountMax:  int(cfg.PinnedDialogsInFolderMax),
+		MeURLPrefix:             cfg.InternalLinksDomain,
+		ChannelsReadMediaPeriod: int(cfg.ChannelsReadMediaPeriod),
+		CallReceiveTimeoutMs:    int(cfg.CallReceiveTimeoutMs),
+		CallRingTimeoutMs:       int(cfg.CallRingTimeoutMs),
+		CallConnectTimeoutMs:    int(cfg.CallConnectTimeoutMs),
+		CallPacketTimeoutMs:     int(cfg.CallPacketTimeoutMs),
+		WebfileDCID:             int(cfg.WebFileDCID),
+		DCTxtDomainName:         cfg.TxtDomainString,
+		PhonecallsEnabled:       cfg.PhoneCallsEnabled,
+		BlockedMode:             cfg.BlockedMode,
+		CaptionLengthMax:        int(cfg.CaptionLengthMax),
+	}
+}
+
 // TDesktopSession converts TDesktop's Account to Data.
-func TDesktopSession(a tdesktop.Account) (*Data, error) {
-	auth := a.Authorization
+func TDesktopSession(account tdesktop.Account) (*Data, error) {
+	auth := account.Authorization
+	cfg := account.Config
 	dc := auth.MainDC
 
 	key, ok := auth.Keys[dc]
@@ -36,15 +88,26 @@ func TDesktopSession(a tdesktop.Account) (*Data, error) {
 	}
 	keyID := key.ID()
 
-	// TODO(tdakkota): distinguish test and production accounts.
-	addr := findDCAddr(dcs.Prod().Options, dc)
+	mappedCfg := mapConfig(dc, cfg)
+	addr := findDCAddr(mappedCfg.DCOptions, dc)
 	if addr == "" {
-		return nil, errors.Errorf("can't find address for DC %d", dc)
+		// Fallback to built-in addrs.
+		var list dcs.List
+		if !cfg.Environment.Test() {
+			list = dcs.Prod()
+		} else {
+			list = dcs.Test()
+		}
+		addr = findDCAddr(list.Options, dc)
+		if addr == "" {
+			return nil, errors.Errorf("can't find address for DC %d", dc)
+		}
 	}
 
 	return &Data{
 		DC:        dc,
 		Addr:      addr,
+		Config:    mappedCfg,
 		AuthKey:   key[:],
 		AuthKeyID: keyID[:],
 	}, nil
diff --git a/session/tdesktop/auth_key.go b/session/tdesktop/auth_key.go
deleted file mode 100644
index fec561c9e..000000000
--- a/session/tdesktop/auth_key.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package tdesktop
-
-import (
-	"io"
-	"math/bits"
-
-	"github.com/ogen-go/errors"
-
-	"github.com/gotd/td/bin"
-	"github.com/gotd/td/internal/crypto"
-)
-
-type reader struct {
-	buf bin.Buffer
-}
-
-func (r *reader) readUint64() (uint64, error) {
-	u, err := r.buf.Uint64()
-	return bits.ReverseBytes64(u), err
-}
-
-func (r *reader) readUint32() (uint32, error) {
-	u, err := r.buf.Uint32()
-	return bits.ReverseBytes32(u), err
-}
-
-func (r *reader) consumeN(target []byte, n int) error {
-	return r.buf.ConsumeN(target, n)
-}
-
-func (r *reader) skip(n int) error {
-	if r.buf.Len() < n {
-		return io.ErrUnexpectedEOF
-	}
-	r.buf.Skip(n)
-	return nil
-}
-
-// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/storage_account.cpp#L898.
-func readMTPData(tgf tdesktopFile, localKey crypto.Key) (MTPAuthorization, error) {
-	encrypted, err := tgf.readArray()
-	if err != nil {
-		return MTPAuthorization{}, errors.Wrap(err, "read encrypted data")
-	}
-
-	decrypted, err := decryptLocal(encrypted, localKey)
-	if err != nil {
-		return MTPAuthorization{}, errors.Wrap(err, "decrypt data")
-	}
-	// Skip decrypted data length (uint32).
-	decrypted = decrypted[4:]
-	r := reader{buf: bin.Buffer{Buf: decrypted}}
-
-	// TODO(tdakkota): support other IDs.
-	var m MTPAuthorization
-	if err := m.deserialize(&r); err != nil {
-		return MTPAuthorization{}, errors.Wrap(err, "deserialize MTPAuthorization")
-	}
-	return m, err
-}
diff --git a/session/tdesktop/constant.go b/session/tdesktop/constant.go
index ec53d328f..fa629532a 100644
--- a/session/tdesktop/constant.go
+++ b/session/tdesktop/constant.go
@@ -2,6 +2,8 @@ package tdesktop
 
 //nolint:revive
 const (
+	kVersion = 1
+
 	localEncryptIterCount      = 4000 // key derivation iteration count
 	localEncryptNoPwdIterCount = 4    // key derivation iteration count without pwd (not secure anyway)
 	localEncryptSaltSize       = 32   // 256 bit
diff --git a/session/tdesktop/dbi.go b/session/tdesktop/dbi.go
index 7f18fa732..6888e3e8d 100644
--- a/session/tdesktop/dbi.go
+++ b/session/tdesktop/dbi.go
@@ -1,5 +1,14 @@
 package tdesktop
 
+import (
+	"io"
+	"math/bits"
+
+	"github.com/ogen-go/errors"
+
+	"github.com/gotd/td/bin"
+)
+
 //nolint:deadcode,unused,varcheck
 const (
 	dbiKey               = 0x00
@@ -78,3 +87,88 @@ const (
 	dbiEncryptedWithSalt = 333
 	dbiEncrypted         = 444
 )
+
+type qtReader struct {
+	buf bin.Buffer
+}
+
+func (r *qtReader) subArray() (qtReader, error) {
+	length, err := r.readInt32()
+	if err != nil {
+		return qtReader{}, errors.Wrap(err, "read length")
+	}
+	sub := bin.Buffer{Buf: r.buf.Buf}
+	if err := r.skip(int(length)); err != nil {
+		return qtReader{}, io.ErrUnexpectedEOF
+	}
+
+	sub.Buf = sub.Buf[:length]
+	return qtReader{buf: sub}, err
+}
+
+func (r *qtReader) readUint64() (uint64, error) {
+	u, err := r.buf.Uint64()
+	return bits.ReverseBytes64(u), err
+}
+
+func (r *qtReader) readUint32() (uint32, error) {
+	u, err := r.buf.Uint32()
+	return bits.ReverseBytes32(u), err
+}
+
+func (r *qtReader) readInt32() (int32, error) {
+	v, err := r.readUint32()
+	return int32(v), err
+}
+
+func (r *qtReader) readString() (string, error) {
+	sz, err := r.readInt32()
+	if err != nil {
+		return "", err
+	}
+	size := int(sz)
+	switch {
+	case size < 0:
+		return "", &bin.InvalidLengthError{
+			Length: size,
+			Where:  "QString",
+		}
+	case size >= r.buf.Len():
+		return "", io.ErrUnexpectedEOF
+	}
+	s := string(r.buf.Buf[:size])
+	r.buf.Skip(size)
+	return s, nil
+}
+
+func (r *qtReader) readBytes() ([]byte, error) {
+	sz, err := r.readInt32()
+	if err != nil {
+		return nil, err
+	}
+	size := int(sz)
+	switch {
+	case size < 0:
+		return nil, &bin.InvalidLengthError{
+			Length: size,
+			Where:  "QString",
+		}
+	case size > r.buf.Len():
+		return nil, io.ErrUnexpectedEOF
+	}
+	s := append([]byte(nil), r.buf.Buf[:size]...)
+	r.buf.Skip(size)
+	return s, nil
+}
+
+func (r *qtReader) consumeN(target []byte, n int) error {
+	return r.buf.ConsumeN(target, n)
+}
+
+func (r *qtReader) skip(n int) error {
+	if r.buf.Len() < n {
+		return io.ErrUnexpectedEOF
+	}
+	r.buf.Skip(n)
+	return nil
+}
diff --git a/session/tdesktop/errors.go b/session/tdesktop/errors.go
index 638967c55..ff0c7b45f 100644
--- a/session/tdesktop/errors.go
+++ b/session/tdesktop/errors.go
@@ -17,6 +17,10 @@ func (w *WrongMagicError) Error() string {
 	return fmt.Sprintf("wrong magic %+v", w.Magic)
 }
 
-// ErrKeyInfoDecrypt is returned when key data decrypt fails.
-// It can happen if passed passcode is wrong.
-var ErrKeyInfoDecrypt = errors.New("key data decrypt")
+var (
+	// ErrKeyInfoDecrypt is returned when key data decrypt fails.
+	// It can happen if passed passcode is wrong.
+	ErrKeyInfoDecrypt = errors.New("key data decrypt")
+	// ErrNoAccounts reports that decoded tdata does not contain any accounts info.
+	ErrNoAccounts = errors.New("tdesktop data does not contain accounts")
+)
diff --git a/session/tdesktop/file.go b/session/tdesktop/file.go
index 4988aef52..3d9f28ec9 100644
--- a/session/tdesktop/file.go
+++ b/session/tdesktop/file.go
@@ -13,17 +13,18 @@ import (
 )
 
 type tdesktopFile struct {
-	io.Reader
+	data    []byte
+	n       int
 	version uint32
 }
 
-func open(filesystem fs.FS, fileName string) (tdesktopFile, error) {
+func open(filesystem fs.FS, fileName string) (*tdesktopFile, error) {
 	suffixes := []string{"0", "1", "s"}
 
-	tryRead := func(p string) (_ tdesktopFile, rErr error) {
+	tryRead := func(p string) (_ *tdesktopFile, rErr error) {
 		f, err := filesystem.Open(p)
 		if err != nil {
-			return tdesktopFile{}, errors.Wrap(err, "open")
+			return nil, errors.Wrap(err, "open")
 		}
 		defer multierr.AppendInvoke(&rErr, multierr.Close(f))
 
@@ -37,7 +38,7 @@ func open(filesystem fs.FS, fileName string) (tdesktopFile, error) {
 				errors.Is(err, fs.ErrPermission) {
 				continue
 			}
-			return tdesktopFile{}, errors.Wrap(err, "stat")
+			return nil, errors.Wrap(err, "stat")
 		}
 
 		f, err := tryRead(p)
@@ -50,23 +51,23 @@ func open(filesystem fs.FS, fileName string) (tdesktopFile, error) {
 			if errors.As(err, &magicErr) {
 				continue
 			}
-			return tdesktopFile{}, errors.Wrap(err, "read tdesktop file")
+			return nil, errors.Wrap(err, "read tdesktop file")
 		}
 
 		return f, nil
 	}
 
-	return tdesktopFile{}, errors.Errorf("file %q not found", fileName)
+	return nil, errors.Errorf("file %q not found", fileName)
 }
 
 var tdesktopFileMagic = [4]byte{'T', 'D', 'F', '$'}
 
 // fromFile creates new Telegram Desktop storage file.
 // Based on https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/details/storage_file_utilities.cpp#L473.
-func fromFile(r io.Reader) (tdesktopFile, error) {
+func fromFile(r io.Reader) (*tdesktopFile, error) {
 	buf := make([]byte, 16)
 	if _, err := io.ReadFull(r, buf[:8]); err != nil {
-		return tdesktopFile{}, errors.Wrap(err, "read magic and version")
+		return nil, errors.Wrap(err, "read magic and version")
 	}
 
 	var magic, version [4]byte
@@ -74,29 +75,29 @@ func fromFile(r io.Reader) (tdesktopFile, error) {
 	// TODO(tdakkota): check version
 	copy(version[:], buf[4:8])
 	if magic != tdesktopFileMagic {
-		return tdesktopFile{}, &WrongMagicError{
+		return nil, &WrongMagicError{
 			Magic: magic,
 		}
 	}
 
 	data, err := io.ReadAll(r)
 	if err != nil {
-		return tdesktopFile{}, errors.Wrap(err, "read data")
+		return nil, errors.Wrap(err, "read data")
 	}
 	if l := len(data); l < 16 {
-		return tdesktopFile{}, errors.Errorf("invalid data length %d", l)
+		return nil, errors.Errorf("invalid data length %d", l)
 	}
 	hash := data[len(data)-16:]
 	data = data[:len(data)-16]
 
 	computedHash := telegramFileHash(data, version)
 	if !bytes.Equal(computedHash[:], hash) {
-		return tdesktopFile{}, errors.New("hash mismatch")
+		return nil, errors.New("hash mismatch")
 	}
 
 	v := binary.LittleEndian.Uint32(version[:])
-	return tdesktopFile{
-		Reader:  bytes.NewBuffer(data),
+	return &tdesktopFile{
+		data:    data,
 		version: v,
 	}, nil
 }
@@ -130,27 +131,30 @@ func telegramFileHash(data []byte, version [4]byte) (r [md5.Size]byte) {
 	return r
 }
 
-func (f tdesktopFile) readArray() ([]byte, error) {
-	return readArray(f.Reader, binary.BigEndian)
+func (f *tdesktopFile) readArray() ([]byte, error) {
+	data, skip, err := readArray(f.data[f.n:], binary.BigEndian)
+	if err != nil {
+		return nil, err
+	}
+	f.n += skip
+	return data, nil
 }
 
-func readArray(reader io.Reader, order binary.ByteOrder) ([]byte, error) {
-	r := make([]byte, 32)
-	if _, err := io.ReadFull(reader, r[:4]); err != nil {
-		return nil, errors.Wrap(err, "read length")
+func readArray(data []byte, order binary.ByteOrder) (array []byte, n int, _ error) {
+	if len(data) < 4 {
+		return nil, 0, io.ErrUnexpectedEOF
 	}
-
 	// See https://github.com/qt/qtbase/blob/5.15.2/src/corelib/text/qbytearray.cpp#L3314.
-	length := order.Uint32(r)
+	length := order.Uint32(data)
 	if length == 0xffffffff {
-		return nil, nil
+		return nil, 4, nil
 	}
 
-	r = append(r[:0], make([]byte, length)...)
-	if _, err := io.ReadFull(reader, r); err != nil {
-		return nil, errors.Wrap(err, "read")
+	if uint64(length) >= uint64(len(data)) {
+		return nil, 0, io.ErrUnexpectedEOF
 	}
-	return r, nil
+	r := data[4 : 4+length]
+	return r, len(r) + 4, nil
 }
 
 func writeArray(writer io.Writer, data []byte, order binary.ByteOrder) error {
diff --git a/session/tdesktop/key_data.go b/session/tdesktop/key_data.go
index 3e9b5f70b..bc097935a 100644
--- a/session/tdesktop/key_data.go
+++ b/session/tdesktop/key_data.go
@@ -1,7 +1,6 @@
 package tdesktop
 
 import (
-	"bytes"
 	"encoding/binary"
 
 	"github.com/ogen-go/errors"
@@ -15,7 +14,7 @@ type keyData struct {
 }
 
 // See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/storage_domain.cpp#L119-L159.
-func readKeyData(tgf tdesktopFile, passcode []byte) (_ keyData, rErr error) {
+func readKeyData(tgf *tdesktopFile, passcode []byte) (_ keyData, rErr error) {
 	salt, err := tgf.readArray()
 	if err != nil {
 		return keyData{}, errors.Wrap(err, "read salt")
@@ -33,7 +32,7 @@ func readKeyData(tgf tdesktopFile, passcode []byte) (_ keyData, rErr error) {
 	if err != nil {
 		return keyData{}, errors.Wrap(err, "decrypt keyEncrypted")
 	}
-	key, err := readArray(bytes.NewReader(keyInnerData), binary.LittleEndian)
+	key, _, err := readArray(keyInnerData, binary.LittleEndian)
 	if err != nil {
 		return keyData{}, errors.Wrap(err, "read key")
 	}
diff --git a/session/tdesktop/mtp_authorization.go b/session/tdesktop/mtp_authorization.go
index 278885cd9..31d020231 100644
--- a/session/tdesktop/mtp_authorization.go
+++ b/session/tdesktop/mtp_authorization.go
@@ -3,20 +3,46 @@ package tdesktop
 import (
 	"github.com/ogen-go/errors"
 
+	"github.com/gotd/td/bin"
 	"github.com/gotd/td/internal/crypto"
 )
 
 // MTPAuthorization is a Telegram Desktop storage structure which stores MTProto session info.
+//
+// See https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/main/main_account.cpp#L359.
 type MTPAuthorization struct {
 	// UserID is a Telegram user ID.
-	UserID int64
+	UserID uint64
 	// MainDC is a main DC ID of this user.
 	MainDC int
 	// Key is a map of keys per DC ID.
 	Keys map[int]crypto.Key // DC ID -> Key
 }
 
-func readKey(r *reader, k *crypto.Key) (uint32, error) {
+// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/storage_account.cpp#L898.
+func readMTPData(tgf *tdesktopFile, localKey crypto.Key) (MTPAuthorization, error) {
+	encrypted, err := tgf.readArray()
+	if err != nil {
+		return MTPAuthorization{}, errors.Wrap(err, "read encrypted data")
+	}
+
+	decrypted, err := decryptLocal(encrypted, localKey)
+	if err != nil {
+		return MTPAuthorization{}, errors.Wrap(err, "decrypt data")
+	}
+	// Skip decrypted data length (uint32).
+	decrypted = decrypted[4:]
+	r := qtReader{buf: bin.Buffer{Buf: decrypted}}
+
+	// TODO(tdakkota): support other IDs.
+	var m MTPAuthorization
+	if err := m.deserialize(&r); err != nil {
+		return MTPAuthorization{}, errors.Wrap(err, "deserialize MTPAuthorization")
+	}
+	return m, err
+}
+
+func readKey(r *qtReader, k *crypto.Key) (uint32, error) {
 	dcID, err := r.readUint32()
 	if err != nil {
 		return 0, errors.Wrap(err, "read DC ID")
@@ -29,7 +55,7 @@ func readKey(r *reader, k *crypto.Key) (uint32, error) {
 	return dcID, nil
 }
 
-func (m *MTPAuthorization) deserialize(r *reader) error {
+func (m *MTPAuthorization) deserialize(r *qtReader) error {
 	id, err := r.readUint32()
 	if err != nil {
 		return errors.Wrap(err, "read dbi ID")
@@ -60,10 +86,10 @@ func (m *MTPAuthorization) deserialize(r *reader) error {
 			return errors.Wrap(err, "read mainDcID")
 		}
 
-		m.UserID = int64(userID)
+		m.UserID = userID
 		m.MainDC = int(mainDC)
 	} else {
-		m.UserID = int64(legacyUserID)
+		m.UserID = uint64(legacyUserID)
 		m.MainDC = int(legacyMainDCID)
 	}
 
diff --git a/session/tdesktop/mtp_config.go b/session/tdesktop/mtp_config.go
new file mode 100644
index 000000000..c4c3d39f0
--- /dev/null
+++ b/session/tdesktop/mtp_config.go
@@ -0,0 +1,308 @@
+package tdesktop
+
+import (
+	"github.com/ogen-go/errors"
+
+	"github.com/gotd/td/bin"
+	"github.com/gotd/td/internal/crypto"
+)
+
+// MTPConfigEnvironment is enum of config environment.
+type MTPConfigEnvironment int32
+
+func (e MTPConfigEnvironment) valid() bool {
+	return e == 0 || e == 1
+}
+
+// String implements fmt.Stringer.
+func (e MTPConfigEnvironment) String() string {
+	switch e {
+	case 0:
+		return "production"
+	case 1:
+		return "test"
+	default:
+		return "unknown"
+	}
+}
+
+// Test denotes that environment is test.
+func (e MTPConfigEnvironment) Test() bool {
+	return e == 1
+}
+
+// MTPConfig is a Telegram Desktop storage structure which stores MTProto config info.
+//
+// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/mtproto/mtproto_config.h.
+type MTPConfig struct {
+	Environment              MTPConfigEnvironment
+	DCOptions                MTPDCOptions
+	ChatSizeMax              int32  // default: 200
+	MegagroupSizeMax         int32  // default: 10000
+	ForwardedCountMax        int32  // default: 100
+	OnlineUpdatePeriod       int32  // default: 120000
+	OfflineBlurTimeout       int32  // default: 5000
+	OfflineIdleTimeout       int32  // default: 30000
+	OnlineFocusTimeout       int32  // default: 1000
+	OnlineCloudTimeout       int32  // default: 300000
+	NotifyCloudDelay         int32  // default: 30000
+	NotifyDefaultDelay       int32  // default: 1500
+	SavedGifsLimit           int32  // default: 200
+	EditTimeLimit            int32  // default: 172800
+	RevokeTimeLimit          int32  // default: 172800
+	RevokePrivateTimeLimit   int32  // default: 172800
+	RevokePrivateInbox       bool   // default: false
+	StickersRecentLimit      int32  // default: 30
+	StickersFavedLimit       int32  // default: 5
+	PinnedDialogsCountMax    int32  // default: 5
+	PinnedDialogsInFolderMax int32  // default: 100
+	InternalLinksDomain      string // default: "https://t.me/"
+	ChannelsReadMediaPeriod  int32  // default: 86400 * 7
+	CallReceiveTimeoutMs     int32  // default: 20000
+	CallRingTimeoutMs        int32  // default: 90000
+	CallConnectTimeoutMs     int32  // default: 30000
+	CallPacketTimeoutMs      int32  // default: 10000
+	WebFileDCID              int32  // default: 4
+	TxtDomainString          string // default: ""
+	PhoneCallsEnabled        bool   // default: true
+	BlockedMode              bool   // default: false
+	CaptionLengthMax         int32  // default: 1024
+}
+
+// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/storage_account.cpp#L938.
+func readMTPConfig(tgf *tdesktopFile, localKey crypto.Key) (MTPConfig, error) {
+	encrypted, err := tgf.readArray()
+	if err != nil {
+		return MTPConfig{}, errors.Wrap(err, "read encrypted data")
+	}
+
+	decrypted, err := decryptLocal(encrypted, localKey)
+	if err != nil {
+		return MTPConfig{}, errors.Wrap(err, "decrypt data")
+	}
+	// Skip decrypted data length (uint32).
+	decrypted = decrypted[4:]
+	root := qtReader{buf: bin.Buffer{Buf: decrypted}}
+
+	cfgReader, err := root.subArray()
+	if err != nil {
+		return MTPConfig{}, errors.Wrap(err, "read config array")
+	}
+
+	var m MTPConfig
+	if err := m.deserialize(&cfgReader); err != nil {
+		return MTPConfig{}, errors.Wrap(err, "deserialize MTPConfig")
+	}
+	return m, err
+}
+
+func (m *MTPConfig) deserialize(r *qtReader) error {
+	version, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read version")
+	}
+	if version != kVersion {
+		return errors.Errorf("wrong version (expected %d, got %d)", kVersion, version)
+	}
+
+	environment, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read environment")
+	}
+	m.Environment = MTPConfigEnvironment(environment)
+	if !m.Environment.valid() {
+		return errors.Errorf("invalid environment %d", environment)
+	}
+
+	{
+		sub, err := r.subArray()
+		if err != nil {
+			return err
+		}
+		if err := m.DCOptions.deserialize(&sub); err != nil {
+			return errors.Wrap(err, "read DC options")
+		}
+	}
+
+	chatSizeMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read chatSizeMax")
+	}
+	m.ChatSizeMax = chatSizeMax
+
+	megagroupSizeMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read megagroupSizeMax")
+	}
+	m.MegagroupSizeMax = megagroupSizeMax
+
+	forwardedCountMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read forwardedCountMax")
+	}
+	m.ForwardedCountMax = forwardedCountMax
+
+	onlineUpdatePeriod, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read onlineUpdatePeriod")
+	}
+	m.OnlineUpdatePeriod = onlineUpdatePeriod
+
+	offlineBlurTimeout, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read offlineBlurTimeout")
+	}
+	m.OfflineBlurTimeout = offlineBlurTimeout
+
+	offlineIdleTimeout, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read offlineIdleTimeout")
+	}
+	m.OfflineIdleTimeout = offlineIdleTimeout
+
+	onlineFocusTimeout, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read onlineFocusTimeout")
+	}
+	m.OnlineFocusTimeout = onlineFocusTimeout
+
+	onlineCloudTimeout, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read onlineCloudTimeout")
+	}
+	m.OnlineCloudTimeout = onlineCloudTimeout
+
+	notifyCloudDelay, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read notifyCloudDelay")
+	}
+	m.NotifyCloudDelay = notifyCloudDelay
+
+	notifyDefaultDelay, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read notifyDefaultDelay")
+	}
+	m.NotifyDefaultDelay = notifyDefaultDelay
+
+	savedGifsLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read savedGifsLimit")
+	}
+	m.SavedGifsLimit = savedGifsLimit
+
+	editTimeLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read editTimeLimit")
+	}
+	m.EditTimeLimit = editTimeLimit
+
+	revokeTimeLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read revokeTimeLimit")
+	}
+	m.RevokeTimeLimit = revokeTimeLimit
+
+	revokePrivateTimeLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read revokePrivateTimeLimit")
+	}
+	m.RevokePrivateTimeLimit = revokePrivateTimeLimit
+
+	revokePrivateInbox, err := r.readUint32()
+	if err != nil {
+		return errors.Wrap(err, "read revokePrivateInbox")
+	}
+	m.RevokePrivateInbox = revokePrivateInbox == 1
+
+	stickersRecentLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read stickersRecentLimit")
+	}
+	m.StickersRecentLimit = stickersRecentLimit
+
+	stickersFavedLimit, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read stickersFavedLimit")
+	}
+	m.StickersFavedLimit = stickersFavedLimit
+
+	pinnedDialogsCountMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read pinnedDialogsCountMax")
+	}
+	m.PinnedDialogsCountMax = pinnedDialogsCountMax
+
+	pinnedDialogsInFolderMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read pinnedDialogsInFolderMax")
+	}
+	m.PinnedDialogsInFolderMax = pinnedDialogsInFolderMax
+
+	internalLinksDomain, err := r.readString()
+	if err != nil {
+		return errors.Wrap(err, "read internalLinksDomain")
+	}
+	m.InternalLinksDomain = internalLinksDomain
+
+	channelsReadMediaPeriod, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read channelsReadMediaPeriod")
+	}
+	m.ChannelsReadMediaPeriod = channelsReadMediaPeriod
+
+	callReceiveTimeoutMs, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read callReceiveTimeoutMs")
+	}
+	m.CallReceiveTimeoutMs = callReceiveTimeoutMs
+
+	callRingTimeoutMs, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read callRingTimeoutMs")
+	}
+	m.CallRingTimeoutMs = callRingTimeoutMs
+
+	callConnectTimeoutMs, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read callConnectTimeoutMs")
+	}
+	m.CallConnectTimeoutMs = callConnectTimeoutMs
+
+	callPacketTimeoutMs, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read callPacketTimeoutMs")
+	}
+	m.CallPacketTimeoutMs = callPacketTimeoutMs
+
+	webFileDCID, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read webFileDCID")
+	}
+	m.WebFileDCID = webFileDCID
+
+	txtDomainString, err := r.readString()
+	if err != nil {
+		return errors.Wrap(err, "read txtDomainString")
+	}
+	m.TxtDomainString = txtDomainString
+
+	phoneCallsEnabled, err := r.readUint32()
+	if err != nil {
+		return errors.Wrap(err, "read phoneCallsEnabled")
+	}
+	m.PhoneCallsEnabled = phoneCallsEnabled == 1
+
+	blockedMode, err := r.readUint32()
+	if err != nil {
+		return errors.Wrap(err, "read blockedMode")
+	}
+	m.BlockedMode = blockedMode == 1
+
+	captionLengthMax, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read captionLengthMax")
+	}
+	m.CaptionLengthMax = captionLengthMax
+
+	return nil
+}
diff --git a/session/tdesktop/mtp_dcoptions.go b/session/tdesktop/mtp_dcoptions.go
new file mode 100644
index 000000000..d187c46e0
--- /dev/null
+++ b/session/tdesktop/mtp_dcoptions.go
@@ -0,0 +1,128 @@
+package tdesktop
+
+import (
+	"github.com/ogen-go/errors"
+
+	"github.com/gotd/td/bin"
+)
+
+// MTPDCOption is a Telegram Desktop storage structure which stores DC info.
+//
+// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/mtproto/mtproto_dc_options.h.
+type MTPDCOption struct {
+	ID     int32
+	Flags  bin.Fields
+	Port   int32
+	IP     string
+	Secret []byte
+}
+
+// IPv6 denotes that the specified IP is an IPv6 address.
+func (m MTPDCOption) IPv6() bool {
+	return m.Flags.Has(0)
+}
+
+// MediaOnly denotes that this DC should only be used to download or upload files.
+func (m MTPDCOption) MediaOnly() bool {
+	return m.Flags.Has(1)
+}
+
+// TCPOOnly denotes that this DC only supports connection with transport obfuscation.
+func (m MTPDCOption) TCPOOnly() bool {
+	return m.Flags.Has(2)
+}
+
+// CDN denotes that this is a CDN DC.
+func (m MTPDCOption) CDN() bool {
+	return m.Flags.Has(3)
+}
+
+// Static denotes that this IP should be used when connecting through a proxy.
+func (m MTPDCOption) Static() bool {
+	return m.Flags.Has(4)
+}
+
+func (m *MTPDCOption) deserialize(r *qtReader, version int32) error {
+	id, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read id")
+	}
+	m.ID = id
+
+	flags, err := r.readUint32()
+	if err != nil {
+		return errors.Wrap(err, "read flags")
+	}
+	m.Flags = bin.Fields(flags)
+
+	port, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read port")
+	}
+	m.Port = port
+
+	const maxIPSize = 45
+	ip, err := r.readString()
+	if err != nil {
+		return errors.Wrap(err, "read ip")
+	}
+	if l := len(ip); l > maxIPSize {
+		return errors.Errorf("too big IP string (%d > %d)", l, maxIPSize)
+	}
+	m.IP = ip
+
+	if version > 0 {
+		const maxSecretSize = 32
+		secret, err := r.readBytes()
+		if err != nil {
+			return errors.Wrap(err, "read secret")
+		}
+		if l := len(secret); l > maxSecretSize {
+			return errors.Errorf("too big DC secret (%d > %d)", l, maxSecretSize)
+		}
+		m.Secret = secret
+	}
+
+	return nil
+}
+
+// MTPDCOptions is a Telegram Desktop storage structure which stores DCs info.
+//
+// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L479.
+type MTPDCOptions struct {
+	Options []MTPDCOption
+}
+
+func (m *MTPDCOptions) deserialize(r *qtReader) error {
+	minusVersion, err := r.readInt32()
+	if err != nil {
+		return errors.Wrap(err, "read version")
+	}
+
+	var version int32
+	if minusVersion < 0 {
+		version = -minusVersion
+	}
+
+	var count int32
+	if version > 0 {
+		c, err := r.readInt32()
+		if err != nil {
+			return errors.Wrap(err, "read count")
+		}
+		count = c
+	} else {
+		count = minusVersion
+	}
+
+	for i := 0; i < int(count); i++ {
+		var o MTPDCOption
+		if err := o.deserialize(r, version); err != nil {
+			return errors.Errorf("read option %d: %w", i, err)
+		}
+		m.Options = append(m.Options, o)
+	}
+
+	// TODO(tdakkota): Read CDN keys.
+	return nil
+}
diff --git a/session/tdesktop/tdesktop.go b/session/tdesktop/tdesktop.go
index d12118ff2..3c9abb169 100644
--- a/session/tdesktop/tdesktop.go
+++ b/session/tdesktop/tdesktop.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io/fs"
 	"os"
+	"path"
 
 	"github.com/ogen-go/errors"
 )
@@ -15,6 +16,8 @@ type Account struct {
 	IDx uint32
 	// Authorization contains Telegram user and MTProto sessions.
 	Authorization MTPAuthorization
+	// Config contains Telegram config.
+	Config MTPConfig
 }
 
 // Read reads accounts info from given Telegram Desktop tdata root.
@@ -38,7 +41,7 @@ func ReadFS(root fs.FS, passcode []byte) ([]Account, error) {
 		return nil, err
 	}
 	if len(kd.accountsIDx) < 1 {
-		return nil, errors.New("tdesktop data does not contain accounts")
+		return nil, ErrNoAccounts
 	}
 
 	r := make([]Account, 0, len(kd.accountsIDx))
@@ -48,19 +51,30 @@ func ReadFS(root fs.FS, passcode []byte) ([]Account, error) {
 			keyFile = fileKey(fmt.Sprintf("data#%d", account+1))
 		}
 
-		tgf, err := open(root, keyFile)
+		mtpConfigFile, err := open(root, path.Join(keyFile, "config"))
+		if err != nil {
+			return nil, errors.Wrap(err, "open mtp config")
+		}
+
+		mtpConfig, err := readMTPConfig(mtpConfigFile, kd.localKey)
+		if err != nil {
+			return nil, errors.Wrap(err, "read mtp config")
+		}
+
+		mtpDataFile, err := open(root, keyFile)
 		if err != nil {
 			return nil, errors.Wrap(err, "open key_data")
 		}
 
-		mtp, err := readMTPData(tgf, kd.localKey)
+		mtpData, err := readMTPData(mtpDataFile, kd.localKey)
 		if err != nil {
 			return nil, errors.Wrap(err, "read mtp")
 		}
 
 		r = append(r, Account{
 			IDx:           account,
-			Authorization: mtp,
+			Authorization: mtpData,
+			Config:        mtpConfig,
 		})
 	}
 


Test patch:
diff --git a/session/tdesktop/file_test.go b/session/tdesktop/file_test.go
index 68d558057..4bf159299 100644
--- a/session/tdesktop/file_test.go
+++ b/session/tdesktop/file_test.go
@@ -12,13 +12,13 @@ import (
 )
 
 var (
-	testData = bytes.Repeat([]byte("abcd"), 4)
-	zeros    = make([]byte, 32)
+	testBytes = bytes.Repeat([]byte("abcd"), 4)
+	zeros     = make([]byte, 32)
 )
 
 func Test_open(t *testing.T) {
 	b := bytes.NewBuffer(nil)
-	if err := writeFile(b, testData, [4]byte{}); err != nil {
+	if err := writeFile(b, testBytes, [4]byte{}); err != nil {
 		t.Fatal(err)
 	}
 
@@ -83,7 +83,7 @@ func Test_fromFile(t *testing.T) {
 		{"InvalidHash", justBytes(tdesktopFileMagic[:], zeros), true},
 		{"WriteFile", func() io.Reader {
 			b := bytes.NewBuffer(nil)
-			if err := writeFile(b, testData, [4]byte{}); err != nil {
+			if err := writeFile(b, testBytes, [4]byte{}); err != nil {
 				t.Fatal(err)
 			}
 			return b
@@ -96,10 +96,7 @@ func Test_fromFile(t *testing.T) {
 				a.Error(err)
 			} else {
 				a.NoError(err)
-
-				data, err := io.ReadAll(r)
-				a.NoError(err)
-				a.Equal(testData, data)
+				a.Equal(testBytes, r.data)
 			}
 		})
 	}
@@ -118,7 +115,7 @@ func Test_telegramFileHash(t *testing.T) {
 
 func Test_readArray(t *testing.T) {
 	var validData bytes.Buffer
-	if err := writeArray(&validData, testData, binary.LittleEndian); err != nil {
+	if err := writeArray(&validData, testBytes, binary.LittleEndian); err != nil {
 		t.Fatal(err)
 	}
 
@@ -131,16 +128,17 @@ func Test_readArray(t *testing.T) {
 	}{
 		{"LengthEOF", nil, nil, binary.LittleEndian, true},
 		{"DataEOF", []byte{255, 0, 0, 0}, nil, binary.LittleEndian, true},
-		{"OK", validData.Bytes(), testData, binary.LittleEndian, false},
+		{"OK", validData.Bytes(), testBytes, binary.LittleEndian, false},
 		{"0xffffffff", []byte{0xff, 0xff, 0xff, 0xff}, nil, binary.LittleEndian, false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			a := require.New(t)
-			if data, err := readArray(bytes.NewReader(tt.data), tt.order); tt.wantErr {
+			if data, n, err := readArray(tt.data, tt.order); tt.wantErr {
 				a.Error(err)
 			} else {
 				a.NoError(err)
+				a.Equal(len(tt.expect)+4, n)
 				a.Equal(tt.expect, data)
 			}
 		})
diff --git a/session/tdesktop/key_data_test.go b/session/tdesktop/key_data_test.go
index 16f71cb91..d3bb5bab6 100644
--- a/session/tdesktop/key_data_test.go
+++ b/session/tdesktop/key_data_test.go
@@ -56,8 +56,8 @@ func Test_readKeyData(t *testing.T) {
 	t.Run("OK", func(t *testing.T) {
 		a := require.New(t)
 
-		kdata, err := readKeyData(tdesktopFile{
-			Reader: bytes.NewReader(fileData),
+		kdata, err := readKeyData(&tdesktopFile{
+			data: fileData,
 		}, passcode)
 		a.NoError(err)
 		a.Equal(localKey, kdata.localKey)
@@ -67,8 +67,8 @@ func Test_readKeyData(t *testing.T) {
 		t.Run(fmt.Sprintf("EOFAfter%d", cut), func(t *testing.T) {
 			a := require.New(t)
 
-			_, err := readKeyData(tdesktopFile{
-				Reader: bytes.NewReader(fileData[:cut]),
+			_, err := readKeyData(&tdesktopFile{
+				data: fileData[:cut],
 			}, passcode)
 			a.Error(err)
 		})
diff --git a/session/tdesktop/mtp_authorization_test.go b/session/tdesktop/mtp_authorization_test.go
index fcc1e8454..f71d00a8d 100644
--- a/session/tdesktop/mtp_authorization_test.go
+++ b/session/tdesktop/mtp_authorization_test.go
@@ -30,8 +30,8 @@ func Test_mtpAuthorization_deserialize(t *testing.T) {
 	t.Run("OK", func(t *testing.T) {
 		a := require.New(t)
 		var m MTPAuthorization
-		a.NoError(m.deserialize(&reader{buf: bin.Buffer{Buf: testData}}))
-		a.Equal(int64(309570373), m.UserID)
+		a.NoError(m.deserialize(&qtReader{buf: bin.Buffer{Buf: testData}}))
+		a.Equal(uint64(309570373), m.UserID)
 		a.Equal(2, m.MainDC)
 		a.Len(m.Keys, 5)
 		for i := 0; i < 5; i++ {
@@ -41,14 +41,14 @@ func Test_mtpAuthorization_deserialize(t *testing.T) {
 	t.Run("WrongID", func(t *testing.T) {
 		a := require.New(t)
 		var m MTPAuthorization
-		a.Error(m.deserialize(&reader{buf: bin.Buffer{Buf: make([]byte, 4)}}))
+		a.Error(m.deserialize(&qtReader{buf: bin.Buffer{Buf: make([]byte, 4)}}))
 	})
 
 	for i := 0; i < maxCut; i += 4 {
 		t.Run(fmt.Sprintf("EOFAfter%d", i), func(t *testing.T) {
 			a := require.New(t)
 			var m MTPAuthorization
-			a.Error(m.deserialize(&reader{buf: bin.Buffer{Buf: testData[:i]}}))
+			a.Error(m.deserialize(&qtReader{buf: bin.Buffer{Buf: testData[:i]}}))
 		})
 	}
 }
diff --git a/session/tdesktop/mtp_dcoptions_test.go b/session/tdesktop/mtp_dcoptions_test.go
new file mode 100644
index 000000000..062c99302
--- /dev/null
+++ b/session/tdesktop/mtp_dcoptions_test.go
@@ -0,0 +1,53 @@
+package tdesktop
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/gotd/td/bin"
+)
+
+var (
+	testIP         = "127.0.0.1"
+	optionTestData = func() []byte {
+		testData := []uint8{
+			0x00, 0x00, 0x00, 0x02, // ID
+			0x00, 0x00, 0x00, 1 << 4, // Flags
+			0x00, 0x00, 0x00, 80, // Port
+			0x00, 0x00, 0x00, uint8(len(testIP)), // IP size
+		}
+
+		// IP
+		testData = append(testData, testIP...)
+		// Secret length
+		testData = append(testData, 0x00, 0x00, 0x00, 0x00)
+
+		return testData
+	}()
+)
+
+func TestMTPDCOption_deserialize(t *testing.T) {
+	maxCut := len(optionTestData)
+
+	t.Run("OK", func(t *testing.T) {
+		a := require.New(t)
+		var m MTPDCOption
+		a.NoError(m.deserialize(&qtReader{buf: bin.Buffer{Buf: optionTestData}}, 1))
+		a.Equal(int32(2), m.ID)
+		a.True(m.Static())
+		a.Equal(bin.Fields(1<<4), m.Flags)
+		a.Equal(int32(80), m.Port)
+		a.Equal(testIP, m.IP)
+	})
+
+	for i := 0; i < maxCut; i += 4 {
+		t.Run(fmt.Sprintf("EOFAfter%d", i), func(t *testing.T) {
+			a := require.New(t)
+			var m MTPDCOption
+			r := &qtReader{buf: bin.Buffer{Buf: optionTestData[:i]}}
+			a.Error(m.deserialize(r, 1))
+		})
+	}
+}
diff --git a/session/tdesktop/tdesktop_example_test.go b/session/tdesktop/tdesktop_example_test.go
index 01b0485e0..457e054c1 100644
--- a/session/tdesktop/tdesktop_example_test.go
+++ b/session/tdesktop/tdesktop_example_test.go
@@ -21,7 +21,8 @@ func ExampleRead() {
 	}
 
 	for _, account := range accounts {
-		a := account.Authorization
-		fmt.Println(a.UserID, a.MainDC)
+		auth := account.Authorization
+		cfg := account.Config
+		fmt.Println(auth.UserID, auth.MainDC, cfg.Environment)
 	}
 }
diff --git a/session/tdesktop_test.go b/session/tdesktop_test.go
index 314e7bf80..78cafbe0b 100644
--- a/session/tdesktop_test.go
+++ b/session/tdesktop_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/gotd/td/internal/crypto"
 	"github.com/gotd/td/session/tdesktop"
 	"github.com/gotd/td/telegram/dcs"
+	"github.com/gotd/td/tg"
 )
 
 func TestTDesktopSession(t *testing.T) {
@@ -28,7 +29,10 @@ func TestTDesktopSession(t *testing.T) {
 				},
 			},
 		}, &Data{
-			DC:        2,
+			DC: 2,
+			Config: tg.Config{
+				ThisDC: 2,
+			},
 			Addr:      findDCAddr(dcs.Prod().Options, 2),
 			AuthKey:   key[:],
 			AuthKeyID: keyID[:],