download: remove file length checks and always check plaintext hash

This commit is contained in:
Tulir Asokan
2026-06-04 23:55:48 +03:00
parent 52afebe984
commit d707fc25c5
4 changed files with 30 additions and 64 deletions
+16 -19
View File
@@ -49,7 +49,10 @@ func (cli *Client) DownloadToFile(ctx context.Context, msg DownloadableMessage,
if len(msg.GetDirectPath()) == 0 {
return ErrNoURLPresent
}
return cli.DownloadMediaWithPathToFile(ctx, msg.GetDirectPath(), msg.GetFileEncSHA256(), msg.GetFileSHA256(), msg.GetMediaKey(), getSize(msg), mediaType, mediaTypeToMMSType[mediaType], false, file)
return cli.DownloadMediaWithPathToFile(
ctx, msg.GetDirectPath(), msg.GetFileEncSHA256(), msg.GetFileSHA256(), msg.GetMediaKey(),
mediaType, mediaTypeToMMSType[mediaType], false, file,
)
}
func (cli *Client) DownloadFBToFile(
@@ -58,18 +61,20 @@ func (cli *Client) DownloadFBToFile(
mediaType MediaType,
file File,
) error {
return cli.DownloadMediaWithPathToFile(ctx, transport.GetDirectPath(), transport.GetFileEncSHA256(), transport.GetFileSHA256(), transport.GetMediaKey(), -1, mediaType, mediaTypeToMMSType[mediaType], false, file)
return cli.DownloadMediaWithPathToFile(
ctx, transport.GetDirectPath(), transport.GetFileEncSHA256(), transport.GetFileSHA256(), transport.GetMediaKey(),
mediaType, mediaTypeToMMSType[mediaType], false, file,
)
}
func (cli *Client) DownloadMediaWithOnlyPathToFile(ctx context.Context, directPath string, file File) error {
return cli.DownloadMediaWithPathToFile(ctx, directPath, nil, nil, nil, -1, "", "", true, file)
return cli.DownloadMediaWithPathToFile(ctx, directPath, nil, nil, nil, "", "", true, file)
}
func (cli *Client) DownloadMediaWithPathToFile(
ctx context.Context,
directPath string,
encFileHash, fileHash, mediaKey []byte,
fileLength int,
mediaType MediaType,
mmsType string,
allowNoHash bool,
@@ -91,9 +96,8 @@ func (cli *Client) DownloadMediaWithPathToFile(
for i, host := range mediaConn.Hosts {
// TODO omit hash for unencrypted media?
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
err = cli.downloadAndDecryptToFile(ctx, mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash, file)
err = cli.downloadAndDecryptToFile(ctx, mediaURL, mediaKey, mediaType, encFileHash, fileHash, file)
if err == nil ||
errors.Is(err, ErrFileLengthMismatch) ||
errors.Is(err, ErrInvalidMediaSHA256) ||
errors.Is(err, ErrMediaDownloadFailedWith403) ||
errors.Is(err, ErrMediaDownloadFailedWith404) ||
@@ -113,7 +117,6 @@ func (cli *Client) downloadAndDecryptToFile(
url string,
mediaKey []byte,
appInfo MediaType,
fileLength int,
fileEncSHA256, fileSHA256 []byte,
file File,
) error {
@@ -141,18 +144,12 @@ func (cli *Client) downloadAndDecryptToFile(
return fmt.Errorf("failed to seek to start of file after validating mac: %w", err)
} else if err = cbcutil.DecryptFile(cipherKey, iv, file); err != nil {
return fmt.Errorf("failed to decrypt file: %w", err)
} else if ReturnDownloadWarnings {
if info, err := file.Stat(); err != nil {
return fmt.Errorf("failed to stat file: %w", err)
} else if fileLength >= 0 && info.Size() != int64(fileLength) {
return fmt.Errorf("%w: expected %d, got %d", ErrFileLengthMismatch, fileLength, info.Size())
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to start of file after decrypting: %w", err)
} else if _, err = io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash file: %w", err)
} else if !hmac.Equal(fileSHA256, hasher.Sum(nil)) {
return ErrInvalidMediaSHA256
}
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to start of file after decrypting: %w", err)
} else if _, err = io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash file: %w", err)
} else if !hmac.Equal(fileSHA256, hasher.Sum(nil)) {
return ErrInvalidMediaSHA256
}
return nil
}
+7 -40
View File
@@ -92,16 +92,6 @@ var (
_ DownloadableMessage = (*types.StickerPackItem)(nil)
)
type downloadableMessageWithLength interface {
DownloadableMessage
GetFileLength() uint64
}
type downloadableMessageWithSizeBytes interface {
DownloadableMessage
GetFileSizeBytes() uint64
}
var classToMediaType = map[protoreflect.Name]MediaType{
"ImageMessage": MediaImage,
"AudioMessage": MediaAudio,
@@ -154,21 +144,6 @@ func (cli *Client) DownloadAny(ctx context.Context, msg *waE2E.Message) (data []
}
}
func getSize(msg DownloadableMessage) int {
switch sized := msg.(type) {
case downloadableMessageWithLength:
return int(sized.GetFileLength())
case downloadableMessageWithSizeBytes:
return int(sized.GetFileSizeBytes())
default:
return -1
}
}
// ReturnDownloadWarnings controls whether the Download function returns non-fatal validation warnings.
// Currently, these include [ErrFileLengthMismatch] and [ErrInvalidMediaSHA256].
var ReturnDownloadWarnings = true
// DownloadThumbnail downloads a thumbnail from a message.
//
// This is primarily intended for downloading link preview thumbnails, which are in ExtendedTextMessage:
@@ -183,7 +158,7 @@ func (cli *Client) DownloadThumbnail(ctx context.Context, msg DownloadableThumbn
} else if len(msg.GetThumbnailDirectPath()) > 0 {
return cli.DownloadMediaWithPath(
ctx, msg.GetThumbnailDirectPath(), msg.GetThumbnailEncSHA256(), msg.GetThumbnailSHA256(), msg.GetMediaKey(),
-1, mediaType, mediaTypeToMMSType[mediaType], false,
mediaType, mediaTypeToMMSType[mediaType], false,
)
} else {
return nil, ErrNoURLPresent
@@ -243,7 +218,7 @@ func (cli *Client) Download(ctx context.Context, msg DownloadableMessage) ([]byt
}
return cli.DownloadMediaWithPath(
ctx, msg.GetDirectPath(), msg.GetFileEncSHA256(), msg.GetFileSHA256(), msg.GetMediaKey(),
getSize(msg), mediaType, mediaTypeToMMSType[mediaType], false,
mediaType, mediaTypeToMMSType[mediaType], false,
)
}
@@ -254,12 +229,12 @@ func (cli *Client) DownloadFB(
) ([]byte, error) {
return cli.DownloadMediaWithPath(
ctx, transport.GetDirectPath(), transport.GetFileEncSHA256(), transport.GetFileSHA256(), transport.GetMediaKey(),
-1, mediaType, mediaTypeToMMSType[mediaType], false,
mediaType, mediaTypeToMMSType[mediaType], false,
)
}
func (cli *Client) DownloadMediaWithOnlyPath(ctx context.Context, directPath string) ([]byte, error) {
return cli.DownloadMediaWithPath(ctx, directPath, nil, nil, nil, -1, "", "", true)
return cli.DownloadMediaWithPath(ctx, directPath, nil, nil, nil, "", "", true)
}
// DownloadMediaWithPath downloads an attachment by manually specifying the path and encryption details.
@@ -267,7 +242,6 @@ func (cli *Client) DownloadMediaWithPath(
ctx context.Context,
directPath string,
encFileHash, fileHash, mediaKey []byte,
fileLength int,
mediaType MediaType,
mmsType string,
allowNoHash bool,
@@ -289,9 +263,8 @@ func (cli *Client) DownloadMediaWithPath(
for i, host := range mediaConn.Hosts {
// TODO omit hash for unencrypted media?
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
data, err = cli.downloadAndDecrypt(ctx, mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash)
data, err = cli.downloadAndDecrypt(ctx, mediaURL, mediaKey, mediaType, encFileHash, fileHash)
if err == nil ||
errors.Is(err, ErrFileLengthMismatch) ||
errors.Is(err, ErrInvalidMediaSHA256) ||
errors.Is(err, ErrMediaDownloadFailedWith403) ||
errors.Is(err, ErrMediaDownloadFailedWith404) ||
@@ -311,7 +284,6 @@ func (cli *Client) downloadAndDecrypt(
url string,
mediaKey []byte,
appInfo MediaType,
fileLength int,
fileEncSHA256,
fileSHA256 []byte,
) (data []byte, err error) {
@@ -329,13 +301,8 @@ func (cli *Client) downloadAndDecrypt(
} else if data, err = cbcutil.Decrypt(cipherKey, iv, ciphertext); err != nil {
err = fmt.Errorf("failed to decrypt file: %w", err)
} else if ReturnDownloadWarnings {
if fileLength >= 0 && len(data) != fileLength {
err = fmt.Errorf("%w: expected %d, got %d", ErrFileLengthMismatch, fileLength, len(data))
} else if len(fileSHA256) != 32 || sha256.Sum256(data) != *(*[32]byte)(fileSHA256) {
// TODO maybe move this out of ReturnDownloadWarnings if whatsapp has started enforcing it in official clients
err = ErrInvalidMediaSHA256
}
} else if len(fileSHA256) != 32 || sha256.Sum256(data) != *(*[32]byte)(fileSHA256) {
err = ErrInvalidMediaSHA256
}
return
}
+3 -1
View File
@@ -132,7 +132,6 @@ var (
ErrMediaDownloadFailedWith404 = DownloadHTTPError{Response: &http.Response{StatusCode: 404}}
ErrMediaDownloadFailedWith410 = DownloadHTTPError{Response: &http.Response{StatusCode: 410}}
ErrNoURLPresent = errors.New("no url present")
ErrFileLengthMismatch = errors.New("file length does not match")
ErrTooShortFile = errors.New("file too short")
ErrInvalidMediaHMAC = errors.New("invalid media hmac")
ErrInvalidMediaEncSHA256 = errors.New("hash of media ciphertext doesn't match")
@@ -140,6 +139,9 @@ var (
ErrInvalidUnencryptedMediaSHA256 = errors.New("hash of unencrypted media doesn't match")
ErrUnknownMediaType = errors.New("unknown media type")
ErrNothingDownloadableFound = errors.New("didn't find any attachments in message")
// Deprecated: this is no longer returned anywhere
ErrFileLengthMismatch = errors.New("file length does not match")
)
var (
Generated
+4 -4
View File
@@ -199,8 +199,8 @@ func (int *DangerousInternalClient) HandleConnectSuccess(ctx context.Context, no
int.c.handleConnectSuccess(ctx, node)
}
func (int *DangerousInternalClient) DownloadAndDecrypt(ctx context.Context, url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSHA256, fileSHA256 []byte) (data []byte, err error) {
return int.c.downloadAndDecrypt(ctx, url, mediaKey, appInfo, fileLength, fileEncSHA256, fileSHA256)
func (int *DangerousInternalClient) DownloadAndDecrypt(ctx context.Context, url string, mediaKey []byte, appInfo MediaType, fileEncSHA256, fileSHA256 []byte) (data []byte, err error) {
return int.c.downloadAndDecrypt(ctx, url, mediaKey, appInfo, fileEncSHA256, fileSHA256)
}
func (int *DangerousInternalClient) DownloadPossiblyEncryptedMediaWithRetries(ctx context.Context, url string, checksum []byte) (file, mac []byte, err error) {
@@ -219,8 +219,8 @@ func (int *DangerousInternalClient) DownloadEncryptedMedia(ctx context.Context,
return int.c.downloadEncryptedMedia(ctx, url, checksum)
}
func (int *DangerousInternalClient) DownloadAndDecryptToFile(ctx context.Context, url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSHA256, fileSHA256 []byte, file File) error {
return int.c.downloadAndDecryptToFile(ctx, url, mediaKey, appInfo, fileLength, fileEncSHA256, fileSHA256, file)
func (int *DangerousInternalClient) DownloadAndDecryptToFile(ctx context.Context, url string, mediaKey []byte, appInfo MediaType, fileEncSHA256, fileSHA256 []byte, file File) error {
return int.c.downloadAndDecryptToFile(ctx, url, mediaKey, appInfo, fileEncSHA256, fileSHA256, file)
}
func (int *DangerousInternalClient) DownloadPossiblyEncryptedMediaWithRetriesToFile(ctx context.Context, url string, checksum []byte, file File) (mac []byte, err error) {