blob: cc88f609c8384b984baa9a87840bfa15ca2b0923 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "formats/jpeg/hasher.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "bindings/input_hasher.h"
#include "crypto/algorithms.h"
#include "formats/byte_range.h"
#include "formats/jpeg/assessor.h"
#include "formats/jpeg/c2pa_segment_handler.h"
#include "formats/jpeg/reader.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/generator.pb.h"
#include "proto/multi_asset_hash_assertion.pb.h"
#include "riegeli/bytes/reader.h"
namespace credentio {
namespace {
constexpr uint64_t kMaxChunkSize = 1024 * 1024; // 1 MiB
// Updates the given hashers with the indicated content, leaving the input
// position at the end of the content.
absl::Status UpdateTwoHashers(riegeli::Reader& input, int64_t offset,
int64_t length, InputHasher& hasher1,
InputHasher& hasher2) {
if (!input.Seek(offset)) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to seek to offset ", offset));
}
int64_t chunk_size = kMaxChunkSize;
for (int64_t i = 0; i < length; i += chunk_size) {
int64_t l = std::min(length - i, chunk_size);
std::string data;
if (!input.Read(l, data)) {
return absl::InvalidArgumentError("Failed to read data");
}
hasher1.Update(data);
hasher2.Update(data);
}
return absl::OkStatus();
}
// Returns true if the current box hash range should end based on the next
// segment header and whether a C2PA segment needs to be injected.
bool InsertC2paBeforeSegment(absl::string_view next_segment_label) {
if (next_segment_label == "SOI") {
return false;
}
if (!absl::StartsWith(next_segment_label, "APP")) {
// Inject before any non-APP segments.
return true;
}
if (next_segment_label == "APP2") {
// Inject before the APP2 segment.
return true;
}
if (next_segment_label == "APP11") {
// Inject before any existing APP11 segments.
return true;
}
return false;
}
absl::StatusOr<uint16_t> GetUnusedSegmentId(
const absl::flat_hash_set<uint16_t>& used_segment_ids) {
if (used_segment_ids.size() == UINT16_MAX) {
return absl::InvalidArgumentError("No Available Segment ID");
}
uint16_t next_segment_id = 1;
while (used_segment_ids.contains(next_segment_id)) {
++next_segment_id;
}
return next_segment_id;
}
// Computes data hash(es) for a JPEG asset (which may be a part of a multi-part
// asset). If the starting input position is 0, then
// * a placeholder exclusion will be added for the C2PA segment
// * the overall file hash will be computed (result.hard_binding.data_hash)
// In all cases, a part hash will be computed, covering the asset starting at
// the current input position and ending at the next EOI marker.
//
// If `exclusion_processor` is provided, it is invoked for each segment to
// determine byte ranges to exclude from the hashes.
absl::Status HashJpeg(riegeli::Reader& input, HashAlgorithm alg,
std::optional<JpegExclusionProcessor> exclusion_processor,
InputHasher& asset_hasher, DigestJpegResult& result) {
int64_t asset_start_offset = input.pos();
bool is_first_asset = asset_start_offset == 0;
C2paSegmentHandler c2pa_segment_handler;
absl::flat_hash_set<uint16_t> used_segment_ids;
std::optional<JpegInsertionPoint> insertion_point;
ABSL_ASSIGN_OR_RETURN(auto part_hasher, InputHasher::Create(alg));
PartBinding* part_binding = result.hard_binding.add_part_bindings();
part_binding->mutable_location()->set_byte_offset(asset_start_offset);
part_binding->set_is_optional(!is_first_asset);
part_binding->mutable_data_hash()->set_alg(absl::StrCat(alg));
std::optional<JpegSegment> data_past_asset = std::nullopt;
ABSL_RETURN_IF_ERROR(IterateOverJpegSegments(
input, [&](const JpegSegment& segment) -> absl::StatusOr<bool> {
if (segment.marker_label == "c2pa.after") {
data_past_asset = std::move(segment);
return false; // stop iteration
}
if (segment.app11_info.has_value()) {
used_segment_ids.insert(segment.app11_info->segment_id);
}
if (is_first_asset && !insertion_point.has_value() &&
InsertC2paBeforeSegment(segment.marker_label)) {
// Add an exclusion placeholder for the C2PA segment. Length will be
// filled in during encoding.
auto* asset_exclusion =
result.hard_binding.mutable_data_hash()->add_exclusions();
asset_exclusion->set_start(segment.offset);
asset_exclusion->set_length(0);
auto* part_exclusion =
part_binding->mutable_data_hash()->add_exclusions();
part_exclusion->set_start(segment.offset - asset_start_offset);
part_exclusion->set_length(0);
insertion_point = JpegInsertionPoint{
.segment_id = 1, // to be updated with the next available ID
.offset = segment.offset,
};
}
std::vector<ByteRange> exclusions;
if (exclusion_processor.has_value()) {
ABSL_ASSIGN_OR_RETURN(exclusions,
(*exclusion_processor)(input, segment));
}
int64_t offset = segment.offset;
for (ByteRange exclusion : exclusions) {
uint64_t segment_end = segment.offset + segment.length;
if (exclusion.offset < offset || exclusion.offset > segment_end ||
exclusion.length > segment_end - exclusion.offset) {
return absl::InvalidArgumentError(absl::StrCat(
"Invalid or overlapping exclusion range [", exclusion.offset,
", ", exclusion.offset + exclusion.length, ") for segment at ",
segment.offset));
}
auto* asset_exclusion =
result.hard_binding.mutable_data_hash()->add_exclusions();
asset_exclusion->set_start(exclusion.offset);
asset_exclusion->set_length(exclusion.length);
auto* part_exclusion =
part_binding->mutable_data_hash()->add_exclusions();
part_exclusion->set_start(exclusion.offset - asset_start_offset);
part_exclusion->set_length(exclusion.length);
ABSL_RETURN_IF_ERROR(UpdateTwoHashers(input, offset,
exclusion.offset - offset,
asset_hasher, *part_hasher));
offset = exclusion.offset + exclusion.length;
}
if (offset < (segment.offset + segment.length)) {
ABSL_RETURN_IF_ERROR(UpdateTwoHashers(
input, offset, segment.offset + segment.length - offset,
asset_hasher, *part_hasher));
}
c2pa_segment_handler.RecordSegment(segment);
return true;
}));
if (is_first_asset) {
absl::Status c2pa_segment_status =
c2pa_segment_handler.GetC2paSegment(input).status();
if (c2pa_segment_status.ok() ||
absl::StrContains(c2pa_segment_status.message(), "multiple")) {
return absl::AlreadyExistsError("C2PA chunk already exists");
}
if (!insertion_point.has_value()) {
return absl::InvalidArgumentError(
"No valid C2PA insertion point found in JPEG");
}
ABSL_ASSIGN_OR_RETURN(uint16_t next_segment_id,
GetUnusedSegmentId(used_segment_ids));
insertion_point->segment_id = next_segment_id;
result.insertion_point = std::move(*insertion_point);
}
if (data_past_asset.has_value()) {
if (!input.Seek(data_past_asset->offset)) {
return absl::InvalidArgumentError("Failed to seek to data past asset");
}
}
part_binding->mutable_location()->set_length(input.pos() -
asset_start_offset);
part_binding->mutable_data_hash()->set_hash(part_hasher->Digest());
return absl::OkStatus();
}
// Adds a part hash covering the remaining bytes of the input, marked as
// optional.
absl::Status HashRemainingBytes(riegeli::Reader& input, HashAlgorithm alg,
InputHasher& asset_hasher,
DigestJpegResult& result) {
if (!input.SupportsSize() || !input.Size().has_value()) {
return absl::InvalidArgumentError("Failed to get size of input");
}
int64_t asset_start_offset = input.pos();
int64_t asset_length = *input.Size() - asset_start_offset;
ABSL_ASSIGN_OR_RETURN(auto part_hasher, InputHasher::Create(alg));
ABSL_RETURN_IF_ERROR(UpdateTwoHashers(input, asset_start_offset, asset_length,
asset_hasher, *part_hasher));
auto* part_binding = result.hard_binding.add_part_bindings();
part_binding->mutable_location()->set_byte_offset(asset_start_offset);
part_binding->mutable_location()->set_length(asset_length);
part_binding->set_is_optional(true);
part_binding->mutable_data_hash()->set_alg(absl::StrCat(alg));
part_binding->mutable_data_hash()->set_hash(part_hasher->Digest());
return absl::OkStatus();
}
} // namespace
absl::StatusOr<DigestJpegResult> DigestJpeg(
riegeli::Reader& input, HashAlgorithm alg, bool generate_multi_asset_hash,
std::optional<JpegExclusionProcessor> exclusion_processor) {
ABSL_ASSIGN_OR_RETURN(auto correct_format, JpegAssessor().IsSupported(input));
if (!correct_format) {
return absl::InvalidArgumentError("File does not start with a valid JPEG");
}
if (!input.Seek(0)) {
return absl::InvalidArgumentError("Failed to seek to start of input");
}
if (!input.SupportsSize() || !input.Size().has_value()) {
return absl::InvalidArgumentError("Failed to get size of input");
}
DigestJpegResult result = {};
result.hard_binding.mutable_data_hash()->set_alg(absl::StrCat(alg));
uint64_t input_size = *input.Size();
ABSL_ASSIGN_OR_RETURN(auto asset_hasher, InputHasher::Create(alg));
int64_t pos;
do {
pos = input.pos();
ABSL_ASSIGN_OR_RETURN(bool is_jpeg, JpegAssessor().IsSupported(input));
if (is_jpeg) {
ABSL_RETURN_IF_ERROR(
HashJpeg(input, alg, exclusion_processor, *asset_hasher, result));
} else {
ABSL_RETURN_IF_ERROR(
HashRemainingBytes(input, alg, *asset_hasher, result));
}
if (!generate_multi_asset_hash) {
break;
}
int64_t newpos = input.pos();
if (pos == newpos && pos < input_size) {
// Avoid infinite loop in case of bugs.
return absl::InternalError("Hashing made no progress");
}
pos = newpos;
} while (pos < input_size);
if (!generate_multi_asset_hash && input.pos() < input_size) {
ABSL_RETURN_IF_ERROR(
asset_hasher->Update(input, input.pos(), input_size - input.pos()));
}
result.hard_binding.mutable_data_hash()->set_hash(asset_hasher->Digest());
if (!generate_multi_asset_hash ||
result.hard_binding.part_bindings_size() == 1) {
// Only a single asset was found, no need for multi-asset hash assertion.
result.hard_binding.clear_part_bindings();
}
return result;
}
} // namespace credentio