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:
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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]}}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user