move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+14
View File
@@ -0,0 +1,14 @@
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
kStrongIterationsCount = 100000
kWideIdsTag = ^uint64(0)
)
+174
View File
@@ -0,0 +1,174 @@
package tdesktop
import (
"io"
"math/bits"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
//nolint:deadcode,unused,varcheck
const (
dbiKey = 0x00
dbiUser = 0x01
dbiDcOptionOldOld = 0x02
dbiChatSizeMax = 0x03
dbiMutePeer = 0x04
dbiSendKey = 0x05
dbiAutoStart = 0x06
dbiStartMinimized = 0x07
dbiSoundNotify = 0x08
dbiWorkMode = 0x09
dbiSeenTrayTooltip = 0x0a
dbiDesktopNotify = 0x0b
dbiAutoUpdate = 0x0c
dbiLastUpdateCheck = 0x0d
dbiWindowPosition = 0x0e
dbiConnectionTypeOld = 0x0f
// 0x10 reserved
dbiDefaultAttach = 0x11
dbiCatsAndDogs = 0x12
dbiReplaceEmojis = 0x13
dbiAskDownloadPath = 0x14
dbiDownloadPathOld = 0x15
dbiScale = 0x16
dbiEmojiTabOld = 0x17
dbiRecentEmojiOldOld = 0x18
dbiLoggedPhoneNumber = 0x19
dbiMutedPeers = 0x1a
// 0x1b reserved
dbiNotifyView = 0x1c
dbiSendToMenu = 0x1d
dbiCompressPastedImage = 0x1e
dbiLangOld = 0x1f
dbiLangFileOld = 0x20
dbiTileBackground = 0x21
dbiAutoLock = 0x22
dbiDialogLastPath = 0x23
dbiRecentEmojiOld = 0x24
dbiEmojiVariantsOld = 0x25
dbiRecentStickers = 0x26
dbiDcOptionOld = 0x27
dbiTryIPv6 = 0x28
dbiSongVolume = 0x29
dbiWindowsNotificationsOld = 0x30
dbiIncludeMuted = 0x31
dbiMegagroupSizeMax = 0x32
dbiDownloadPath = 0x33
dbiAutoDownload = 0x34
dbiSavedGifsLimit = 0x35
dbiShowingSavedGifsOld = 0x36
dbiAutoPlay = 0x37
dbiAdaptiveForWide = 0x38
dbiHiddenPinnedMessages = 0x39
dbiRecentEmoji = 0x3a
dbiEmojiVariants = 0x3b
dbiDialogsMode = 0x40
dbiModerateMode = 0x41
dbiVideoVolume = 0x42
dbiStickersRecentLimit = 0x43
dbiNativeNotifications = 0x44
dbiNotificationsCount = 0x45
dbiNotificationsCorner = 0x46
dbiThemeKey = 0x47
dbiDialogsWidthRatioOld = 0x48
dbiUseExternalVideoPlayer = 0x49
dbiDcOptions = 0x4a
dbiMtpAuthorization = 0x4b
dbiLastSeenWarningSeenOld = 0x4c
dbiAuthSessionSettings = 0x4d
dbiLangPackKey = 0x4e
dbiConnectionType = 0x4f
dbiStickersFavedLimit = 0x50
dbiSuggestStickersByEmoji = 0x51
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
}
+26
View File
@@ -0,0 +1,26 @@
package tdesktop
import (
"fmt"
"github.com/go-faster/errors"
)
// WrongMagicError is returned when tdesktop data file
// has wrong magic header.
type WrongMagicError struct {
Magic [4]byte
}
// Error implements error.
func (w *WrongMagicError) Error() string {
return fmt.Sprintf("wrong magic %+v", w.Magic)
}
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")
)
+12
View File
@@ -0,0 +1,12 @@
package tdesktop
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestWrongMagicError_Error(t *testing.T) {
w := &WrongMagicError{}
require.NotEmpty(t, w.Error())
}
+177
View File
@@ -0,0 +1,177 @@
package tdesktop
import (
"bytes"
"crypto/md5" // #nosec G501
"encoding/binary"
"io"
"io/fs"
"math"
"github.com/go-faster/errors"
"go.uber.org/multierr"
)
type tdesktopFile struct {
data []byte
n int
version uint32
}
func open(filesystem fs.FS, fileName string) (*tdesktopFile, error) {
suffixes := []string{"0", "1", "s"}
tryRead := func(p string) (_ *tdesktopFile, rErr error) {
f, err := filesystem.Open(p)
if err != nil {
return nil, errors.Wrap(err, "open")
}
defer multierr.AppendInvoke(&rErr, multierr.Close(f))
return fromFile(f)
}
for _, suffix := range suffixes {
p := fileName + suffix
if _, err := fs.Stat(filesystem, p); err != nil {
if errors.Is(err, fs.ErrNotExist) ||
errors.Is(err, fs.ErrPermission) {
continue
}
return nil, errors.Wrap(err, "stat")
}
f, err := tryRead(p)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
continue
}
var magicErr *WrongMagicError
if errors.As(err, &magicErr) {
continue
}
return nil, errors.Wrap(err, "read tdesktop file")
}
return f, nil
}
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) {
buf := make([]byte, 16)
if _, err := io.ReadFull(r, buf[:8]); err != nil {
return nil, errors.Wrap(err, "read magic and version")
}
var magic, version [4]byte
copy(magic[:], buf[:4])
// TODO(tdakkota): check version
copy(version[:], buf[4:8])
if magic != tdesktopFileMagic {
return nil, &WrongMagicError{
Magic: magic,
}
}
data, err := io.ReadAll(r)
if err != nil {
return nil, errors.Wrap(err, "read data")
}
if l := len(data); l < 16 {
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 nil, errors.New("hash mismatch")
}
v := binary.LittleEndian.Uint32(version[:])
return &tdesktopFile{
data: data,
version: v,
}, nil
}
func writeFile(w io.Writer, data []byte, version [4]byte) error {
if _, err := w.Write(tdesktopFileMagic[:]); err != nil {
return errors.Wrap(err, "write magic")
}
if _, err := w.Write(version[:]); err != nil {
return errors.Wrap(err, "write version")
}
if _, err := w.Write(data); err != nil {
return errors.Wrap(err, "write data")
}
hash := telegramFileHash(data, version)
if _, err := w.Write(hash[:]); err != nil {
return errors.Wrap(err, "write hash")
}
return nil
}
func telegramFileHash(data []byte, version [4]byte) (r [md5.Size]byte) {
h := md5.New() // #nosec G401
_, _ = h.Write(data)
var packedLength [4]byte
binary.LittleEndian.PutUint32(packedLength[:], uint32(len(data)))
_, _ = h.Write(packedLength[:])
_, _ = h.Write(version[:])
_, _ = h.Write(tdesktopFileMagic[:])
h.Sum(r[:0])
return r
}
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(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(data)
if length == 0xffffffff {
return nil, 4, nil
}
if uint64(length) >= uint64(len(data)) {
return nil, 0, io.ErrUnexpectedEOF
}
r := data[4 : 4+length]
return r, len(r) + 4, nil
}
func writeArray(writer io.Writer, data []byte, order binary.ByteOrder) error {
length := len(data)
if uint64(length) > uint64(math.MaxUint32) {
return errors.Errorf("data length too big (%d)", length)
}
r := make([]byte, 4)
order.PutUint32(r, uint32(length))
if _, err := writer.Write(r); err != nil {
return errors.Wrap(err, "write length")
}
if _, err := writer.Write(data); err != nil {
return errors.Wrap(err, "write data")
}
return nil
}
+146
View File
@@ -0,0 +1,146 @@
package tdesktop
import (
"bytes"
"encoding/binary"
"io"
"io/fs"
"testing"
"testing/fstest"
"github.com/stretchr/testify/require"
)
var (
testBytes = bytes.Repeat([]byte("abcd"), 4)
zeros = make([]byte, 32)
)
func Test_open(t *testing.T) {
b := bytes.NewBuffer(nil)
if err := writeFile(b, testBytes, [4]byte{}); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
fs fs.FS
fileName string
wantErr bool
}{
{"Empty", fstest.MapFS{}, "testfile", true},
{"SkipInvalidLength", fstest.MapFS{
// open should skip first file and read next
"testfile0": &fstest.MapFile{
Data: zeros[:4],
},
"testfile1": &fstest.MapFile{
Data: b.Bytes(),
},
}, "testfile", false},
{"SkipInvalidMagic", fstest.MapFS{
// open should skip first file and read next
"testfile0": &fstest.MapFile{
Data: zeros,
},
"testfile1": &fstest.MapFile{
Data: b.Bytes(),
},
}, "testfile", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
if _, err := open(tt.fs, tt.fileName); tt.wantErr {
a.Error(err)
} else {
a.NoError(err)
}
})
}
}
func Test_fromFile(t *testing.T) {
justBytes := func(data []byte, datas ...[]byte) func() io.Reader {
return func() (r io.Reader) {
r = bytes.NewReader(data)
for _, d := range datas {
r = io.MultiReader(r, bytes.NewReader(d))
}
return r
}
}
tests := []struct {
name string
data func() io.Reader
wantErr bool
}{
{"Empty", justBytes(nil), true},
{"InvalidMagic", justBytes(zeros), true},
{"InvalidLength", justBytes(tdesktopFileMagic[:], zeros[:8]), true},
{"InvalidHash", justBytes(tdesktopFileMagic[:], zeros), true},
{"WriteFile", func() io.Reader {
b := bytes.NewBuffer(nil)
if err := writeFile(b, testBytes, [4]byte{}); err != nil {
t.Fatal(err)
}
return b
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
if r, err := fromFile(tt.data()); tt.wantErr {
a.Error(err)
} else {
a.NoError(err)
a.Equal(testBytes, r.data)
}
})
}
}
func Test_telegramFileHash(t *testing.T) {
data := bytes.Repeat([]byte{'a'}, 100)
expected := [16]uint8{
0xa8, 0xa9, 0xa1, 0x38,
0xcf, 0x32, 0x37, 0xa9,
0x4b, 0x78, 0xd0, 0x2d,
0x03, 0xe0, 0x16, 0x81,
}
require.Equal(t, expected, telegramFileHash(data, [4]byte{0, 0, 0, 1}))
}
func Test_readArray(t *testing.T) {
var validData bytes.Buffer
if err := writeArray(&validData, testBytes, binary.LittleEndian); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
data []byte
expect []byte
order binary.ByteOrder
wantErr bool
}{
{"LengthEOF", nil, nil, binary.LittleEndian, true},
{"DataEOF", []byte{255, 0, 0, 0}, nil, binary.LittleEndian, true},
{"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, 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)
}
})
}
}
+71
View File
@@ -0,0 +1,71 @@
package tdesktop
import (
"encoding/binary"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
)
type keyData struct {
localKey crypto.Key
accountsIDx []uint32
}
// 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) {
salt, err := tgf.readArray()
if err != nil {
return keyData{}, errors.Wrap(err, "read salt")
}
if l := len(salt); l != localEncryptSaltSize {
return keyData{}, errors.Errorf("invalid salt length %d", l)
}
passcodeKey := createLocalKey(passcode, salt)
keyEncrypted, err := tgf.readArray()
if err != nil {
return keyData{}, errors.Wrap(err, "read keyEncrypted")
}
keyInnerData, err := decryptLocal(keyEncrypted, passcodeKey)
if err != nil {
return keyData{}, errors.Wrap(err, "decrypt keyEncrypted")
}
key, _, err := readArray(keyInnerData, binary.LittleEndian)
if err != nil {
return keyData{}, errors.Wrap(err, "read key")
}
if l := len(key); l < len(crypto.Key{}) {
return keyData{}, errors.Errorf("key too small (%d)", l)
}
var localKey crypto.Key
copy(localKey[:], key)
infoEncrypted, err := tgf.readArray()
if err != nil {
return keyData{}, errors.Wrap(err, "read infoEncrypted")
}
infoDecrypted, err := decryptLocal(infoEncrypted, localKey)
if err != nil {
return keyData{}, ErrKeyInfoDecrypt
}
// Skip decrypted data length.
infoDecrypted = infoDecrypted[4:]
// Read count of accounts.
count := int(binary.BigEndian.Uint32(infoDecrypted))
infoDecrypted = infoDecrypted[4:]
// Preallocate accountsIDx.
accountsIDx := make([]uint32, 0, count)
for i := 0; i < count; i++ {
idx := binary.BigEndian.Uint32(infoDecrypted[i*4:])
accountsIDx = append(accountsIDx, idx)
}
return keyData{
localKey: localKey,
accountsIDx: accountsIDx,
}, nil
}
@@ -0,0 +1,76 @@
package tdesktop
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func Test_readKeyData(t *testing.T) {
a := require.New(t)
buf := bytes.Buffer{}
var (
passcode []byte
salt = make([]byte, 32)
info = []byte{
16, 0, 0, 0,
0, 0, 0, 1,
0, 0, 0, 0,
0, 0, 0, 0,
}
passcodeKey = createLocalKey(passcode, salt)
localKey = createLocalKey([]byte("aboba"), salt)
// Store buffer offsets to test EOF errors
cuts = []int{0}
)
// Write salt.
a.NoError(writeArray(&buf, salt, binary.BigEndian))
cuts = append(cuts, buf.Len())
var keyInnerData []byte
{
b := bytes.Buffer{}
// Pad to 16 bytes.
data := make([]byte, 272-4)
copy(data, localKey[:])
a.NoError(writeArray(&b, data, binary.LittleEndian))
keyInnerData = b.Bytes()
}
keyEncrypted, err := encryptLocal(keyInnerData, passcodeKey)
a.NoError(err)
a.NoError(writeArray(&buf, keyEncrypted, binary.BigEndian))
cuts = append(cuts, buf.Len())
infoEncrypted, err := encryptLocal(info, localKey)
a.NoError(err)
a.NoError(writeArray(&buf, infoEncrypted, binary.BigEndian))
// Do not store last cut, because it is valid.
fileData := buf.Bytes()
t.Run("OK", func(t *testing.T) {
a := require.New(t)
kdata, err := readKeyData(&tdesktopFile{
data: fileData,
}, passcode)
a.NoError(err)
a.Equal(localKey, kdata.localKey)
a.Len(kdata.accountsIDx, 1)
})
for _, cut := range cuts {
t.Run(fmt.Sprintf("EOFAfter%d", cut), func(t *testing.T) {
a := require.New(t)
_, err := readKeyData(&tdesktopFile{
data: fileData[:cut],
}, passcode)
a.Error(err)
})
}
}
+93
View File
@@ -0,0 +1,93 @@
package tdesktop
import (
"bytes"
"crypto/aes"
"crypto/sha1" // #nosec G505
"crypto/sha512"
"github.com/go-faster/errors"
"golang.org/x/crypto/pbkdf2"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
)
// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/details/storage_file_utilities.cpp#L322.
func createLegacyLocalKey(passcode, salt []byte) (r crypto.Key) {
iters := localEncryptNoPwdIterCount
if len(passcode) > 0 {
iters = localEncryptIterCount
}
key := pbkdf2.Key(passcode, salt, iters, len(r), sha1.New)
copy(r[:], key)
return r
}
// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/details/storage_file_utilities.cpp#L300.
func createLocalKey(passcode, salt []byte) (r crypto.Key) {
iters := 1
if len(passcode) > 0 {
iters = kStrongIterationsCount
}
h := sha512.New()
_, _ = h.Write(salt)
_, _ = h.Write(passcode)
_, _ = h.Write(salt)
key := pbkdf2.Key(h.Sum(nil), salt, iters, len(r), sha512.New)
copy(r[:], key)
return r
}
// See https://github.com/telegramdesktop/tdesktop/blob/v2.9.8/Telegram/SourceFiles/storage/details/storage_file_utilities.cpp#L584.
func decryptLocal(encrypted []byte, localKey crypto.Key) ([]byte, error) {
if l := len(encrypted); l%aes.BlockSize != 0 {
return nil, errors.Errorf("invalid length %d, must be padded to 16", l)
}
// Get encryptedKey.
var msgKey bin.Int128
n := copy(msgKey[:], encrypted)
encrypted = encrypted[n:]
aesKey, aesIV := crypto.OldKeys(localKey, msgKey, crypto.Server)
cipher, err := aes.NewCipher(aesKey[:])
if err != nil {
return nil, errors.Wrap(err, "create cipher")
}
decrypted := make([]byte, len(encrypted))
ige.DecryptBlocks(cipher, aesIV[:], decrypted, encrypted)
if h := sha1.Sum(decrypted); !bytes.Equal(h[:16], msgKey[:]) /* #nosec G401 */ {
return nil, errors.New("msg_key mismatch")
}
return decrypted, nil
}
// encryptLocal code may panic
func encryptLocal(decrypted []byte, localKey crypto.Key) ([]byte, error) {
if l := len(decrypted); l%aes.BlockSize != 0 {
return nil, errors.Errorf("invalid length %d, must be padded to 16", l)
}
// Compute encryptedKey.
var msgKey bin.Int128
h := sha1.Sum(decrypted) // #nosec G401
copy(msgKey[:], h[:])
aesKey, aesIV := crypto.OldKeys(localKey, msgKey, crypto.Server)
cipher, err := aes.NewCipher(aesKey[:])
if err != nil {
return nil, errors.Wrap(err, "create cipher")
}
encrypted := make([]byte, 16+len(decrypted))
copy(encrypted, msgKey[:])
ige.EncryptBlocks(cipher, aesIV[:], encrypted[16:], decrypted)
return encrypted, nil
}
+183
View File
@@ -0,0 +1,183 @@
package tdesktop
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
)
func Test_createLegacyLocalKey(t *testing.T) {
tests := []struct {
name string
pass, salt []byte
output string
}{
{
"NoPasscode",
nil,
[]byte("salt"),
"49c9b4503692e6d97dd4dc009d25f0c3ba18f2c24ba2247" +
"63e9e9b7a105defea90a7af133c8877199219be40aad81df80" +
"3785f07b4ad88cc4a03be6946a3aca1fc74d6bbb74d39d975c" +
"59cda120226493b4937ca99c933423ee15352c8e76efc9c3dc" +
"b4f5d4ee9f123ee5e339ccfe3c84909290a002bc91d29fa27f" +
"66fb736d22bd6d4ab7a5020d31dcd0d491042d78522f2470a4" +
"4281cc9315856e1528d5abbe1d78573230d73516eedce9598c" +
"dee1052f73e154fce79d9934a66e1b52b1d598861648a4f9d9" +
"5a958a5f527c896db63ff7e1dae0db16c66c36ba984faf65f3" +
"36fdb4f7efcbaee7f89bf634ef084bbf6e46d91f8ceaf4052e" +
"9ea20f49bf243dc",
},
{
"pass",
[]byte("pass"),
[]byte("salt"),
"b08653719bf59a6a7c8eb1abae9c267e6e0252a9ea54683" +
"806d093c2f1dff9cea4341b3728bca217389026afe6c7b69eb" +
"9affc6e3ced50b07e0168fc4ad2cef468f06def70cc932b7e6" +
"024f3c92bf3f650ed49df4460b0fbd30358c57c4db14ac7ddb" +
"755dff9d1b0b7c664e11bd3460f0f772a9ac1afd880d3be01c" +
"8b39ccd44e96248226cbe5623436abb0ef26071eafbc7b8cfb" +
"1db72c982dd7a61baa2669ada459e2c6d67ad5e7c1445ed48e" +
"0b8e3ec4fbeb5126bec2175508acb9e0e1f9aa2f7ea888e519" +
"85b410e41da33fc38d765cb00ace54860069edfc8a35c9650c" +
"754989defefc785772fd7eb017b1ef351cf3abcc839ce2c995" +
"5981d555bbaefad",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
expected, err := hex.DecodeString(tt.output)
a.NoError(err)
k := createLegacyLocalKey(tt.pass, tt.salt)
a.Equal(expected, k[:])
})
}
}
func Test_createLocalKey(t *testing.T) {
tests := []struct {
name string
pass, salt []byte
output string
}{
{
"NoPasscode",
nil,
[]byte("salt"),
"bd73811ec41b37e1bbe6484bbb42cb775d3aa83a453a80" +
"380b35f6773319b789a92ca49a8f4607ce412e7955e5916c8" +
"8047b237e60e4bcc28d0a21628f07cefad449f996ac42ab30" +
"25b80005f9d5d75c12e4927782b12ed77ce15c96e1a44a6bf" +
"65dfa67e8228d7351b12336692223cee72d697f226cfee229" +
"54196856100d7e1cfc70b0b04deb30190502f3438e06530e1" +
"8253d4c3d87daa1d1a0ad27e537f49baf6835cc6b2cf701e7" +
"fb8a457d04bd092372c9fc5d9b4cc8be2a62a979333eb736a" +
"2e72b6b6e8da385117092e9a4eb0797098e9f2f156f0cdbcd" +
"ea5c27d5e2decf1bb383e7b8568ed1f384bf84de414a07595" +
"6498c6903d4ac6612c43b7eea",
},
{
"pass",
[]byte("pass"),
[]byte("salt"),
"54f00fbe5fbd1ddbc42f290e892032f780dff189d759fc4" +
"5f1bb14d03db1a6a37a7ba27402dd53a3429657afc293ff26f" +
"15b4df1351502386844e0ab213f4662ffa7dd5e60b0e06abfd" +
"5ee0d6b7a266a86cbf3aa5edaa92ab3e992aa20a31becf860f" +
"b48689310144c6d1a9d98f90b84675b7fe00c41782e940db04" +
"f6bb84babdd350f1d45fc3bb2073d42f36ba47bdfe93d4c969" +
"9291b62d0e7bc01765886a9475f412420d9609903f6a654425" +
"63de1dbea16f6c8d758ed43a6ccc3cb7fe9c5f9a0df8285deb" +
"db25e1d1dd39ecd584a41b161b39ff1becd42735bc44118d39" +
"d71aa4ffce8e8559a6b901a17379620e1fadea76cb51a4ff18" +
"b7b962ee6076375",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
expected, err := hex.DecodeString(tt.output)
a.NoError(err)
k := createLocalKey(tt.pass, tt.salt)
a.Equal(expected, k[:])
})
}
}
func Test_cryptLocal(t *testing.T) {
key := crypto.Key{
0xe1, 0x6f, 0x98, 0xd9, 0xf0, 0x7e, 0x58, 0x58,
0xda, 0x7a, 0xf3, 0xf9, 0xd0, 0x04, 0x1a, 0xa0,
0x11, 0xd2, 0x33, 0xd1, 0x7e, 0x58, 0xa9, 0x05,
0xd0, 0x82, 0x11, 0x97, 0xa5, 0x6b, 0xd0, 0x69,
0x3d, 0x86, 0x79, 0xff, 0xef, 0x63, 0x20, 0xec,
0xbf, 0x56, 0xa1, 0xf6, 0x12, 0x68, 0xd1, 0xd8,
0xb8, 0x4d, 0x16, 0x15, 0x46, 0xe7, 0x1a, 0x4b,
0xc3, 0x8d, 0x7a, 0x25, 0x59, 0x7a, 0xee, 0xef,
0x55, 0xed, 0x01, 0x65, 0x55, 0xf1, 0x66, 0xc5,
0xe0, 0x65, 0x5f, 0x26, 0xee, 0x40, 0x1c, 0xee,
0x53, 0x4e, 0xd4, 0xa2, 0x67, 0xc7, 0x7a, 0xaf,
0x23, 0x90, 0x31, 0x2b, 0xd2, 0xdd, 0xb5, 0xa9,
0x40, 0xb5, 0xd1, 0x1d, 0x5e, 0x6c, 0xbf, 0x6f,
0xe4, 0xb8, 0x66, 0xf3, 0x5b, 0xac, 0x1c, 0x7c,
0xb0, 0x0d, 0x16, 0x27, 0xa3, 0x97, 0xa0, 0xdc,
0x2b, 0xc4, 0x18, 0x8c, 0xf1, 0xe3, 0x5c, 0x6f,
0x9f, 0xa2, 0xb2, 0x05, 0x87, 0x03, 0x70, 0xec,
0xe6, 0x12, 0x7c, 0x36, 0x17, 0xfc, 0xc2, 0x5c,
0x6c, 0x2f, 0xcc, 0x0f, 0x4f, 0x2c, 0xa5, 0xcc,
0x08, 0xa5, 0x4e, 0x8b, 0xb0, 0xba, 0xb9, 0x29,
0x6c, 0x02, 0x79, 0xb2, 0x2d, 0x73, 0xbd, 0x8b,
0x1e, 0x9a, 0x49, 0x11, 0x9d, 0xa8, 0x88, 0xe8,
0xb9, 0x1c, 0x32, 0x67, 0x4d, 0xf2, 0x2c, 0xa4,
0x72, 0xa5, 0x0a, 0xdd, 0x60, 0xe3, 0xb2, 0x01,
0x52, 0x38, 0x8e, 0xe9, 0x7b, 0x96, 0xa4, 0xbb,
0x24, 0x0a, 0x13, 0x8f, 0x79, 0x23, 0xcc, 0x8b,
0x82, 0x1a, 0xfb, 0xaa, 0x1e, 0xf3, 0xbe, 0x51,
0xaa, 0xa3, 0x14, 0x83, 0x25, 0x11, 0x0e, 0xcc,
0x7e, 0x99, 0xba, 0x37, 0x60, 0x4b, 0x72, 0x69,
0x8e, 0xfe, 0xa4, 0xed, 0x56, 0xbf, 0xe5, 0x54,
0x45, 0xe7, 0x2e, 0x2b, 0x55, 0x6b, 0x13, 0xe3,
0xca, 0xe9, 0xe6, 0xa9, 0xb9, 0x89, 0xb0, 0x72,
}
t.Run("PaddingCheck", func(t *testing.T) {
a := require.New(t)
_, err := encryptLocal([]byte{1}, key)
a.Error(err)
_, err = decryptLocal([]byte{1}, key)
a.Error(err)
})
t.Run("DecryptEncryptDecrypt", func(t *testing.T) {
expectEncrypted := []uint8{
0x0a, 0x9e, 0x69, 0xc6, 0xd8, 0x79, 0x69, 0x12,
0xae, 0xd6, 0xa4, 0x89, 0xe6, 0xb9, 0xf1, 0xdd,
0x7a, 0xea, 0x4e, 0x5a, 0x49, 0x7e, 0x6e, 0xe5,
0xc2, 0xb4, 0x05, 0x05, 0x11, 0xd9, 0xda, 0x9d,
}
expectDecrypted := []uint8{
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
a := require.New(t)
decrypted, err := decryptLocal(expectEncrypted, key)
a.NoError(err)
a.Equal(expectDecrypted, decrypted)
encrypted, err := encryptLocal(decrypted, key)
a.NoError(err)
a.Equal(expectEncrypted, encrypted)
decrypted2, err := decryptLocal(encrypted, key)
a.NoError(err)
a.Equal(expectDecrypted, decrypted2)
})
}
+20
View File
@@ -0,0 +1,20 @@
package tdesktop
import (
"crypto/md5" // #nosec G501
"encoding/hex"
"strings"
)
func tdesktopMD5(s string) string {
hash := md5.Sum([]byte(s)) // #nosec G401
for i := range hash {
hash[i] = hash[i]<<4 | hash[i]>>4
}
hexed := hex.EncodeToString(hash[:])
return strings.ToUpper(hexed)
}
func fileKey(s string) string {
return tdesktopMD5(s)[:16]
}
+24
View File
@@ -0,0 +1,24 @@
package tdesktop
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_tdesktopMD5(t *testing.T) {
tests := []struct {
input, output string
}{
{"data", "D877F783D5D3EF8C18D5027F940662CD"},
{"data#1", "438B3BB129B86F4E9CA19D4CDF58C398"},
{"data#2", "A7FDF864FBC10B7772ADA161AE3C900E"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
a := require.New(t)
a.Equal(tt.output, tdesktopMD5(tt.input))
a.Equal(tt.output[:16], fileKey(tt.input))
})
}
}
@@ -0,0 +1,115 @@
package tdesktop
import (
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/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 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
}
// 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")
}
if err := r.consumeN(k[:], 256); err != nil {
return 0, errors.Wrap(err, "read auth key")
}
return dcID, nil
}
func (m *MTPAuthorization) deserialize(r *qtReader) error {
id, err := r.readUint32()
if err != nil {
return errors.Wrap(err, "read dbi ID")
}
if id != dbiMtpAuthorization {
return errors.Errorf("unexpected id %d", id)
}
if err := r.skip(4); err != nil {
return errors.Wrap(err, "read mainLength")
}
legacyUserID, err := r.readUint32()
if err != nil {
return errors.Wrap(err, "read legacyUserID")
}
legacyMainDCID, err := r.readUint32()
if err != nil {
return errors.Wrap(err, "read legacyMainDCID")
}
if (uint64(legacyUserID)<<32)|uint64(legacyMainDCID) == kWideIdsTag {
userID, err := r.readUint64()
if err != nil {
return errors.Wrap(err, "read userID")
}
mainDC, err := r.readUint32()
if err != nil {
return errors.Wrap(err, "read mainDcID")
}
m.UserID = userID
m.MainDC = int(mainDC)
} else {
m.UserID = uint64(legacyUserID)
m.MainDC = int(legacyMainDCID)
}
keys, err := r.readUint32()
if err != nil {
return errors.Wrap(err, "read keys length")
}
if m.Keys == nil {
m.Keys = make(map[int]crypto.Key, keys)
}
for i := 0; i < int(keys); i++ {
var key crypto.Key
dcID, err := readKey(r, &key)
if err != nil {
return errors.Wrapf(err, "read key %d", i)
}
// FIXME(tdakkota): what if there is more than one session per DC?
m.Keys[int(dcID)] = key
}
return nil
}
@@ -0,0 +1,54 @@
package tdesktop
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func Test_mtpAuthorization_deserialize(t *testing.T) {
testData := []uint8{
0x00, 0x00, 0x00, 0x4b, // dbiMtpAuthorization
0x00, 0x00, 0x05, 0x30, // mainLength
0xff, 0xff, 0xff, 0xff, // legacyUserId
0xff, 0xff, 0xff, 0xff, // legacyMainDcId
0x00, 0x00, 0x00, 0x00, 0x12, 0x73, 0xab, 0x45, // UserID = 309570373
0x00, 0x00, 0x00, 0x02, // DC = 2
0x00, 0x00, 0x00, 0x05, // 5 keys.
}
maxCut := len(testData) + 16
for i := byte(0); i < 5; i++ {
testData = append(testData, 0, 0, 0, i) // DC ID as BigEndian uint32
key := bytes.Repeat([]byte{i}, 256)
testData = append(testData, key...)
}
t.Run("OK", func(t *testing.T) {
a := require.New(t)
var m MTPAuthorization
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++ {
a.Equal(m.Keys[i][0], uint8(i))
}
})
t.Run("WrongID", func(t *testing.T) {
a := require.New(t)
var m MTPAuthorization
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(&qtReader{buf: bin.Buffer{Buf: testData[:i]}}))
})
}
}
+308
View File
@@ -0,0 +1,308 @@
package tdesktop
import (
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
+128
View File
@@ -0,0 +1,128 @@
package tdesktop
import (
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
@@ -0,0 +1,53 @@
package tdesktop
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/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))
})
}
}
+80
View File
@@ -0,0 +1,80 @@
// Package tdesktop contains Telegram Desktop session decoder.
package tdesktop
import (
"fmt"
"io/fs"
"os"
"path"
"github.com/go-faster/errors"
)
// Account is a Telegram user account representation in Telegram Desktop storage.
type Account struct {
// IDx is an internal Telegram Desktop account ID.
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.
// Shorthand for:
//
// ReadFS(os.DirFS(root), passcode)
func Read(root string, passcode []byte) ([]Account, error) {
return ReadFS(os.DirFS(root), passcode)
}
// ReadFS reads Telegram Desktop accounts info from given FS root.
func ReadFS(root fs.FS, passcode []byte) ([]Account, error) {
keyDataFile, err := open(root, "key_data")
if err != nil {
return nil, errors.Wrap(err, "open key_data")
}
kd, err := readKeyData(keyDataFile, passcode)
if err != nil {
return nil, err
}
if len(kd.accountsIDx) < 1 {
return nil, ErrNoAccounts
}
r := make([]Account, 0, len(kd.accountsIDx))
for _, account := range kd.accountsIDx {
var keyFile = fileKey("data")
if account > 0 {
keyFile = fileKey(fmt.Sprintf("data#%d", account+1))
}
mtpDataFile, err := open(root, keyFile)
if err != nil {
return nil, errors.Wrap(err, "open key_data")
}
mtpData, err := readMTPData(mtpDataFile, kd.localKey)
if err != nil {
return nil, errors.Wrap(err, "read mtp")
}
a := Account{
IDx: account,
Authorization: mtpData,
}
mtpConfigFile, err := open(root, path.Join(keyFile, "config"))
if err == nil {
mtpConfig, err := readMTPConfig(mtpConfigFile, kd.localKey)
// HACK: ignoring error, because config is optional.
if err == nil {
a.Config = mtpConfig
}
}
r = append(r, a)
}
return r, nil
}
@@ -0,0 +1,28 @@
package tdesktop_test
import (
"fmt"
"os"
"path/filepath"
"go.mau.fi/mautrix-telegram/pkg/gotd/session/tdesktop"
)
func ExampleRead() {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
root := filepath.Join(home, "Downloads", "Telegram", "tdata")
accounts, err := tdesktop.Read(root, nil)
if err != nil {
panic(err)
}
for _, account := range accounts {
auth := account.Authorization
cfg := account.Config
fmt.Println(auth.UserID, auth.MainDC, cfg.Environment)
}
}