Files
mautrix-telegram/pkg/gotd/telegram/downloader/cdn.go
T
2025-06-27 20:03:37 -07:00

99 lines
2.6 KiB
Go

package downloader
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// ExpiredTokenError error is returned when Downloader get expired file token for CDN.
// See https://core.telegram.org/constructor/upload.fileCdnRedirect.
type ExpiredTokenError struct {
*tg.UploadCDNFileReuploadNeeded
}
// Error implements error interface.
func (r *ExpiredTokenError) Error() string {
return "redirect to master DC for requesting new file token"
}
// cdn is a CDN DC download schema.
// See https://core.telegram.org/cdn#getting-files-from-a-cdn.
type cdn struct {
cdn CDN
client Client
pool *bin.Pool
redirect *tg.UploadFileCDNRedirect
}
var _ schema = cdn{}
// decrypt decrypts file chunk from Telegram CDN.
// See https://core.telegram.org/cdn#decrypting-files.
func (c cdn) decrypt(src []byte, offset int64) ([]byte, error) {
block, err := aes.NewCipher(c.redirect.EncryptionKey)
if err != nil {
return nil, errors.Wrap(err, "create cipher")
}
if block.BlockSize() != len(c.redirect.EncryptionIv) {
return nil, errors.Errorf(
"invalid IV or key length, block size %d != IV %d",
block.BlockSize(), len(c.redirect.EncryptionIv),
)
}
// Copy IV to buffer from Pool.
iv := c.pool.GetSize(len(c.redirect.EncryptionIv))
defer c.pool.Put(iv)
copy(iv.Buf, c.redirect.EncryptionIv)
// For IV, it should use the value of encryption_iv, modified in the following manner:
// for each offset replace the last 4 bytes of the encryption_iv with offset / 16 in big-endian.
binary.BigEndian.PutUint32(iv.Buf[iv.Len()-4:], uint32(offset/16))
dst := make([]byte, len(src))
cipher.NewCTR(block, iv.Buf).XORKeyStream(dst, src)
return dst, nil
}
func (c cdn) Chunk(ctx context.Context, offset int64, limit int) (chunk, error) {
r, err := c.cdn.UploadGetCDNFile(ctx, &tg.UploadGetCDNFileRequest{
Offset: offset,
Limit: limit,
FileToken: c.redirect.FileToken,
})
if err != nil {
return chunk{}, err
}
switch result := r.(type) {
case *tg.UploadCDNFile:
data, err := c.decrypt(result.Bytes, offset)
if err != nil {
return chunk{}, err
}
return chunk{
data: data,
}, nil
case *tg.UploadCDNFileReuploadNeeded:
return chunk{}, &ExpiredTokenError{UploadCDNFileReuploadNeeded: result}
default:
return chunk{}, errors.Errorf("unexpected type %T", r)
}
}
func (c cdn) Hashes(ctx context.Context, offset int64) ([]tg.FileHash, error) {
return c.client.UploadGetCDNFileHashes(ctx, &tg.UploadGetCDNFileHashesRequest{
FileToken: c.redirect.FileToken,
Offset: offset,
})
}