JP2 - stronger checks on Signatuer and FileType boxes

This commit is contained in:
Luis Díaz Más 2022-03-18 14:54:42 +01:00
parent 2b9f6ccf6b
commit 1545a1bc4a
6 changed files with 197 additions and 76 deletions

View File

@ -17,6 +17,7 @@ add_library( exiv2lib_int OBJECT
fujimn_int.cpp fujimn_int.hpp
helper_functions.cpp helper_functions.hpp
image_int.cpp image_int.hpp
jp2image_int.cpp jp2image_int.hpp
makernote_int.cpp makernote_int.hpp
minoltamn_int.cpp minoltamn_int.hpp
nikonmn_int.cpp nikonmn_int.hpp

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// included header files
#include "jp2image.hpp"
#include "config.h"
#include "basicio.hpp"
@ -9,7 +11,7 @@
#include "futils.hpp"
#include "image.hpp"
#include "image_int.hpp"
#include "jp2image.hpp"
#include "jp2image_int.hpp"
#include "safe_op.hpp"
#include "tiffimage.hpp"
#include "types.hpp"
@ -28,8 +30,6 @@ constexpr uint32_t kJp2BoxTypeColorSpec = 0x636f6c72; // Color Specification
constexpr uint32_t kJp2BoxTypeUuid = 0x75756964; // 'uuid'
constexpr uint32_t kJp2BoxTypeClose = 0x6a703263; // 'jp2c'
const uint32_t brandJp2 = 0x6a703220;
// JPEG-2000 UUIDs for embedded metadata
//
// See http://www.jpeg.org/public/wg1n2600.doc for information about embedding IPTC-NAA data in JPEG-2000 files
@ -40,11 +40,11 @@ constexpr unsigned char kJp2UuidIptc[] = "\x33\xc7\xa4\xd2\xb8\x1d\x47\x23\xa0\x
constexpr unsigned char kJp2UuidXmp[] = "\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac";
// See section B.1.1 (JPEG 2000 Signature box) of JPEG-2000 specification
constexpr unsigned char Jp2Signature[] = {
constexpr std::array<byte, 12> Jp2Signature{
0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a,
};
constexpr unsigned char Jp2Blank[] = {
constexpr std::array<byte, 249> Jp2Blank{
0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a, 0x00, 0x00, 0x00, 0x14, 0x66, 0x74,
0x79, 0x70, 0x6a, 0x70, 0x32, 0x20, 0x00, 0x00, 0x00, 0x00, 0x6a, 0x70, 0x32, 0x20, 0x00, 0x00, 0x00, 0x2d,
0x6a, 0x70, 0x32, 0x68, 0x00, 0x00, 0x00, 0x16, 0x69, 0x68, 0x64, 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
@ -61,26 +61,7 @@ constexpr unsigned char Jp2Blank[] = {
0x00, 0x00, 0xff, 0x93, 0xcf, 0xb4, 0x04, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0xff, 0xd9,
};
struct Jp2BoxHeader {
uint32_t length;
uint32_t type;
};
struct Jp2ImageHeaderBox {
uint32_t imageHeight;
uint32_t imageWidth;
uint16_t componentCount;
uint8_t bpc; //<! Bits per component
uint8_t c; //<! Compression type
uint8_t unkC; //<! Colourspace unknown
uint8_t ipr; //<! Intellectual property
};
struct Jp2UuidBox {
uint8_t uuid[16];
};
const size_t boxHSize = sizeof(Jp2BoxHeader);
const size_t boxHSize = sizeof(Internal::Jp2BoxHeader);
void lf(std::ostream& out, bool& bLF) {
if (bLF) {
@ -128,7 +109,7 @@ Jp2Image::Jp2Image(BasicIo::UniquePtr io, bool create) : Image(ImageType::jp2, m
std::cerr << "Exiv2::Jp2Image:: Creating JPEG2000 image to memory" << std::endl;
#endif
IoCloser closer(*io_);
if (io_->write(Jp2Blank, sizeof(Jp2Blank)) != sizeof(Jp2Blank)) {
if (io_->write(Jp2Blank.data(), Jp2Blank.size()) != Jp2Blank.size()) {
#ifdef EXIV2_DEBUG_MESSAGES
std::cerr << "Exiv2::Jp2Image:: Failed to create JPEG2000 image on memory" << std::endl;
#endif
@ -153,16 +134,19 @@ void Jp2Image::readMetadata() {
throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
}
IoCloser closer(*io_);
if (!isJp2Type(*io_, true)) {
if (!isJp2Type(*io_, false)) {
throw Error(ErrorCode::kerNotAnImage, "JPEG-2000");
}
Jp2BoxHeader box = {0, 0};
Jp2BoxHeader subBox = {0, 0};
Jp2ImageHeaderBox ihdr = {0, 0, 0, 0, 0, 0, 0};
Jp2UuidBox uuid = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
Internal::Jp2BoxHeader box = {0, 0};
Internal::Jp2BoxHeader subBox = {0, 0};
Internal::Jp2ImageHeaderBox ihdr = {0, 0, 0, 0, 0, 0, 0};
Internal::Jp2UuidBox uuid = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
size_t boxesCount = 0;
size_t boxem = 1000; // boxes max
const size_t boxem = 1000; // boxes max
uint32_t lastBoxTypeRead = 0;
bool boxSignatureFound = false;
bool boxFileTypeFound = false;
while (io_->read(reinterpret_cast<byte*>(&box), boxHSize) == boxHSize) {
boxes_check(boxesCount++, boxem);
@ -180,30 +164,25 @@ void Jp2Image::readMetadata() {
return;
if (box.length == 1) {
/// \todo In this case, the real box size is given in bytes XLBox (bytes 8-15)
/// \todo In this case, the real box size is given in XLBox (bytes 8-15)
}
switch (box.type) {
case kJp2BoxTypeSignature: {
#ifdef EXIV2_DEBUG_MESSAGES
std::cout << "Exiv2::Jp2Image::readMetadata: JPEG 2000 Signature box found" << std::endl;
#endif
if (boxSignatureFound) // Only one is allowed
throw Error(ErrorCode::kerCorruptedMetadata);
boxSignatureFound = true;
break;
}
case kJp2BoxTypeFileTypeBox: {
// This box shall immediately follow the JPEG 2000 Signature box
/// \todo All files shall contain one and only one File Type box.
assert(box.length >= 20); // 8 (box) + 4 (BR) + 4(MinV) + >=4 (CLn)
DataBuf data(box.length - boxHSize);
io_->read(data.data(), data.size());
const uint32_t brand = data.read_uint32(0, bigEndian);
const uint32_t minorVersion = data.read_uint32(4, bigEndian);
const uint32_t compatibilityList = data.read_uint32(8, bigEndian);
// const size_t clCount = (data.size() - 8) / 4;
// for(size_t i = 0; i < clCount; i++) {
// uint32_t compatibilityList = data.read_uint32(8 + i*4, bigEndian);
// }
if (brand != brandJp2 || minorVersion != 0 || compatibilityList != brandJp2)
if (boxFileTypeFound || lastBoxTypeRead != kJp2BoxTypeSignature) { // Only one is allowed
throw Error(ErrorCode::kerCorruptedMetadata);
}
boxFileTypeFound = true;
std::vector<byte> boxData(box.length - boxHSize);
io_->readOrThrow(boxData.data(), boxData.size(), ErrorCode::kerCorruptedMetadata);
if (!Internal::isValidBoxFileType(boxData))
throw Error(ErrorCode::kerCorruptedMetadata);
break;
}
@ -397,6 +376,7 @@ void Jp2Image::readMetadata() {
break;
}
}
lastBoxTypeRead = box.type;
// Move to the next box.
io_->seek(static_cast<long>(position - boxHSize + box.length), BasicIo::beg);
@ -422,6 +402,7 @@ void Jp2Image::printStructure(std::ostream& out, PrintStructureOption option, in
bool bICC = option == kpsIccProfile;
bool bXMP = option == kpsXMP;
bool bIPTCErase = option == kpsIptcErase;
bool boxSignatureFound = false;
if (bPrint) {
out << "STRUCTURE OF JPEG2000 FILE: " << io_->path() << std::endl;
@ -429,9 +410,9 @@ void Jp2Image::printStructure(std::ostream& out, PrintStructureOption option, in
}
if (bPrint || bXMP || bICC || bIPTCErase) {
Jp2BoxHeader box = {1, 1};
Jp2BoxHeader subBox = {1, 1};
Jp2UuidBox uuid = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
Internal::Jp2BoxHeader box = {1, 1};
Internal::Jp2BoxHeader subBox = {1, 1};
Internal::Jp2UuidBox uuid = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
bool bLF = false;
while (box.length && box.type != kJp2BoxTypeClose &&
@ -453,24 +434,17 @@ void Jp2Image::printStructure(std::ostream& out, PrintStructureOption option, in
switch (box.type) {
case kJp2BoxTypeSignature: {
/// \todo we should make sure that only 1 of this boxes is found
assert(box.length == 12);
DataBuf data(4);
io_->read(data.data(), data.size());
if (data.read_uint32(0, bigEndian) != 0x0D0A870A) {
if (boxSignatureFound) // Only one is allowed
throw Error(ErrorCode::kerCorruptedMetadata);
}
boxSignatureFound = true;
break;
}
case kJp2BoxTypeFileTypeBox: {
// This box shall immediately follow the JPEG 2000 Signature box
/// \todo All files shall contain one and only one File Type box.
DataBuf data(12);
io_->read(data.data(), data.size());
uint32_t brand = data.read_uint32(0, bigEndian);
uint32_t minorVersion = data.read_uint32(4, bigEndian);
uint32_t compatibilityList = data.read_uint32(8, bigEndian);
if (brand != brandJp2 || minorVersion != 0 || compatibilityList != brandJp2)
std::vector<byte> boxData(box.length - boxHSize);
io_->readOrThrow(boxData.data(), boxData.size(), ErrorCode::kerCorruptedMetadata);
if (!Internal::isValidBoxFileType(boxData))
throw Error(ErrorCode::kerCorruptedMetadata);
break;
}
@ -629,7 +603,7 @@ void Jp2Image::encodeJp2Header(const DataBuf& boxBuf, DataBuf& outBuf) {
size_t outlen = boxHSize; // now many bytes have we written to output?
long inlen = boxHSize; // how many bytes have we read from boxBuf?
enforce(boxHSize <= output.size(), ErrorCode::kerCorruptedMetadata);
auto pBox = reinterpret_cast<const Jp2BoxHeader*>(boxBuf.c_data());
auto pBox = reinterpret_cast<const Internal::Jp2BoxHeader*>(boxBuf.c_data());
uint32_t length = getLong(reinterpret_cast<const byte*>(&pBox->length), bigEndian);
enforce(length <= output.size(), ErrorCode::kerCorruptedMetadata);
uint32_t count = boxHSize;
@ -638,12 +612,12 @@ void Jp2Image::encodeJp2Header(const DataBuf& boxBuf, DataBuf& outBuf) {
while (count < length && !bWroteColor) {
enforce(boxHSize <= length - count, ErrorCode::kerCorruptedMetadata);
auto pSubBox = reinterpret_cast<const Jp2BoxHeader*>(p + count);
auto pSubBox = reinterpret_cast<const Internal::Jp2BoxHeader*>(p + count);
// copy data. pointer could be into a memory mapped file which we will decode!
Jp2BoxHeader subBox;
Internal::Jp2BoxHeader subBox;
memcpy(&subBox, pSubBox, boxHSize);
Jp2BoxHeader newBox = subBox;
Internal::Jp2BoxHeader newBox = subBox;
if (count < length) {
subBox.length = getLong(reinterpret_cast<byte*>(&subBox.length), bigEndian);
@ -696,7 +670,7 @@ void Jp2Image::encodeJp2Header(const DataBuf& boxBuf, DataBuf& outBuf) {
// allocate the correct number of bytes, copy the data and update the box header
outBuf.alloc(outlen);
outBuf.copyBytes(0, output.c_data(), outlen);
auto oBox = reinterpret_cast<Jp2BoxHeader*>(outBuf.data());
auto oBox = reinterpret_cast<Internal::Jp2BoxHeader*>(outBuf.data());
ul2Data(reinterpret_cast<byte*>(&oBox->type), kJp2BoxTypeHeader, bigEndian);
ul2Data(reinterpret_cast<byte*>(&oBox->length), static_cast<uint32_t>(outlen), bigEndian);
}
@ -722,14 +696,14 @@ void Jp2Image::doWriteMetadata(BasicIo& outIo) {
}
// Write JPEG2000 Signature (This is the 1st box)
if (outIo.write(Jp2Signature, 12) != 12)
if (outIo.write(Jp2Signature.data(), Jp2Signature.size()) != 12)
throw Error(ErrorCode::kerImageWriteFailed);
#ifdef EXIV2_DEBUG_MESSAGES
std::cout << "Jp2Image::doWriteMetadata: JPEG 2000 Signature box written" << std::endl;
#endif
Jp2BoxHeader box = {0, 0};
Internal::Jp2BoxHeader box = {0, 0};
byte boxDataSize[4];
byte boxUUIDtype[4];
@ -917,15 +891,14 @@ Image::UniquePtr newJp2Instance(BasicIo::UniquePtr io, bool create) {
}
bool isJp2Type(BasicIo& iIo, bool advance) {
const int32_t len = 12;
byte buf[len];
const size_t bytesRead = iIo.read(buf, len);
if (iIo.error() || iIo.eof() || bytesRead != len) {
byte buf[Jp2Signature.size()];
const size_t bytesRead = iIo.read(buf, Jp2Signature.size());
if (iIo.error() || iIo.eof() || bytesRead != Jp2Signature.size()) {
return false;
}
bool matched = (memcmp(buf, Jp2Signature, len) == 0);
bool matched = (memcmp(buf, Jp2Signature.data(), Jp2Signature.size()) == 0);
if (!advance || !matched) {
iIo.seek(-len, BasicIo::cur); // Return to original position
iIo.seek(-Jp2Signature.size(), BasicIo::cur); // Return to original position
}
return matched;
}

32
src/jp2image_int.cpp Normal file
View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "jp2image_int.hpp"
#include "error.hpp"
#include "types.hpp"
#include <cassert>
namespace Exiv2::Internal {
bool isValidBoxFileType(const std::vector<uint8_t> &boxData) {
// BR & MinV are obligatory (4 + 4 bytes). Afterwards we have N compatibility lists (of size 4)
if ((boxData.size() - 8u) % 4u != 0) {
return false;
}
const size_t N = (boxData.size() - 8u) / 4u;
const uint32_t brand = getULong(boxData.data(), bigEndian);
const uint32_t minorVersion = getULong(boxData.data() + 4, bigEndian);
bool clWithRightBrand = false;
for (size_t i = 0; i < N; i++) {
uint32_t compatibilityList = getULong(boxData.data() + 8 + i * 4, bigEndian);
if (compatibilityList == brandJp2) {
clWithRightBrand = true;
break;
}
}
return (brand == brandJp2 && minorVersion == 0 && clWithRightBrand);
}
} // namespace Exiv2::Internal

36
src/jp2image_int.hpp Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef JP2IMAGE_INT_HPP
#define JP2IMAGE_INT_HPP
#include <cstdint>
#include <vector>
namespace Exiv2::Internal {
struct Jp2BoxHeader {
uint32_t length;
uint32_t type;
};
struct Jp2ImageHeaderBox {
uint32_t imageHeight;
uint32_t imageWidth;
uint16_t componentCount;
uint8_t bpc; //<! Bits per component
uint8_t c; //<! Compression type
uint8_t unkC; //<! Colourspace unknown
uint8_t ipr; //<! Intellectual property
};
struct Jp2UuidBox {
uint8_t uuid[16];
};
constexpr uint32_t brandJp2{0x6a703220};
/// @brief Determines if the File Type box is valid
bool isValidBoxFileType(const std::vector<std::uint8_t>& boxData);
} // namespace Exiv2::Internal
#endif // JP2IMAGE_INT_HPP

View File

@ -15,6 +15,7 @@ add_executable(unit_tests
test_image_int.cpp
test_ImageFactory.cpp
test_jp2image.cpp
test_jp2image_int.cpp
test_IptcKey.cpp
test_LangAltValueRead.cpp
test_pngimage.cpp

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "jp2image_int.hpp" // Internals of JPEG-2000 standard
#include <gtest/gtest.h>
using namespace Exiv2::Internal;
namespace {
void setValidValues(std::vector<std::uint8_t>& boxData) {
// The first 4 bytes correspond to the BR (Brand). It must have the value 'jp2\040'
boxData[0] = 'j';
boxData[1] = 'p';
boxData[2] = '2';
boxData[3] = '\040';
// The next 4 bytes correspond to the MinV (Minor version). It is a 4-byte unsigned int with value 0
// The only available Compatibility list also has the value 'jp2\040'
boxData[8] = 'j';
boxData[9] = 'p';
boxData[10] = '2';
boxData[11] = '\040';
}
} // namespace
TEST(Jp2_FileTypeBox, isNotValidWithoutProperValuesSet) {
const std::vector<std::uint8_t> boxData(12);
ASSERT_FALSE(isValidBoxFileType(boxData));
}
TEST(Jp2_FileTypeBox, isValidWithMinimumPossibleSizeAndValidValues) {
std::vector<std::uint8_t> boxData(12);
setValidValues(boxData);
ASSERT_TRUE(isValidBoxFileType(boxData));
}
TEST(Jp2_FileTypeBox, isNotValidWithMinimumPossibleSizeButInvalidBrand) {
std::vector<std::uint8_t> boxData(12);
setValidValues(boxData);
boxData[2] = '3'; // Change byte in the brand field
ASSERT_FALSE(isValidBoxFileType(boxData));
}
TEST(Jp2_FileTypeBox, isNotValidWithMinimumPossibleSizeButInvalidCL1) {
std::vector<std::uint8_t> boxData(12);
setValidValues(boxData);
boxData[10] = '3'; // Change byte in the CL1
ASSERT_FALSE(isValidBoxFileType(boxData));
}
// ----------------------------------------------------------
TEST(Jp2_FileTypeBox, withInvalidBoxDataSizeIsInvalid) {
std::vector<std::uint8_t> boxData(13); // 12 + 1 (the extra byte causes problems)
ASSERT_FALSE(isValidBoxFileType(boxData));
}
TEST(Jp2_FileTypeBox, with2CLs_lastOneWithBrandValue_isValid) {
std::vector<std::uint8_t> boxData(16);
// The first 4 bytes correspond to the BR (Brand). It must have the value 'jp2\040'
boxData[0] = 'j';
boxData[1] = 'p';
boxData[2] = '2';
boxData[3] = '\040';
// The next 4 bytes correspond to the MinV (Minor version). It is a 4-byte unsigned int with value 0
// The 2nd Compatibility list has the value 'jp2\040'
boxData[12] = 'j';
boxData[13] = 'p';
boxData[14] = '2';
boxData[15] = '\040';
ASSERT_TRUE(isValidBoxFileType(boxData));
}