blob: 597396541709d291eaf5b3ba30ab8eb81a0b9ebf [file]
// 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/riff/writer.h"
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "bindings/input_hasher.h"
#include "crypto/algorithms.h"
#include "formats/hasher_data.h"
#include "formats/riff/constants.h"
#include "formats/riff/reader.h"
#include "proto/generator.pb.h"
#include "riegeli/base/byte_fill.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_writer.h"
#include "riegeli/bytes/writer.h"
#include "riegeli/endian/endian_writing.h"
#include "utils/byte_instruction.h"
namespace credentio {
namespace {
absl::StatusOr<uint64_t> FindC2paInsertionPoint(riegeli::Reader& input) {
uint64_t c2pa_insertion_point = 0;
bool first = true;
ABSL_RETURN_IF_ERROR(IterateOverRiffChunks(
input,
[&c2pa_insertion_point,
&first](const RiffChunk& chunk) -> absl::StatusOr<bool> {
if (first) {
// Verify that the input starts with RIFF chunk.
if (chunk.id != kRiffChunkIdRiff) {
return absl::InvalidArgumentError(
"Input does not start with RIFF chunk");
}
first = false;
c2pa_insertion_point = chunk.offset + chunk.length;
return true;
}
if (chunk.id == kRiffChunkIdC2pa) {
return absl::AlreadyExistsError("C2PA chunk already exists");
}
if (chunk.offset >= c2pa_insertion_point) {
return false; // Stop iterating, we finished the first RIFF chunk
}
return true;
}));
if (first) {
return absl::InvalidArgumentError("Input is empty");
}
return c2pa_insertion_point;
}
absl::Status AddRiffSizeInstruction(
uint32_t riff_size, std::vector<ByteInstruction>& byte_instructions) {
byte_instructions.push_back({
.operation = ByteInstruction::Operation::kReplace,
.offset = 4,
.bytes = "",
});
riegeli::StringWriter size_writer(&byte_instructions.back().bytes);
if (!riegeli::WriteLittleEndian<uint32_t>(riff_size, size_writer)) {
return absl::InternalError("failed to create size writer");
}
if (!size_writer.Close()) {
return absl::InternalError("failed to write updated RIFF size");
}
return absl::OkStatus();
}
absl::Status AddC2paManifestInstructionPlaceholder(
uint64_t c2pa_insertion_point, uint64_t manifest_store_size,
uint64_t pad_size, std::vector<ByteInstruction>& byte_instructions) {
byte_instructions.push_back({
.operation = ByteInstruction::Operation::kInsert,
.offset = c2pa_insertion_point,
.bytes = "",
});
riegeli::StringWriter c2pa_writer(&byte_instructions.back().bytes);
if (!c2pa_writer.Write(kRiffChunkIdC2pa)) {
return absl::InternalError("failed to write C2PA chunk ID");
}
if (!riegeli::WriteLittleEndian<uint32_t>(manifest_store_size + pad_size,
c2pa_writer)) {
return absl::InternalError("failed to write C2PA chunk size");
}
if (pad_size > 0) {
if (!c2pa_writer.Write(riegeli::ByteFill(pad_size, '\0'))) {
return absl::InternalError("failed to write padding");
}
}
if (!c2pa_writer.Close()) {
return absl::InternalError("failed to close c2pa writer");
}
return absl::OkStatus();
}
absl::StatusOr<std::string> ComputeHash(
riegeli::Reader& source, uint64_t file_size,
absl::Span<const ByteInstruction> instructions) {
if (!source.Seek(0) || source.pos() != 0) {
return absl::InternalError("failed to seek to start of source");
}
ABSL_ASSIGN_OR_RETURN(std::unique_ptr<InputHasher> hasher,
InputHasher::Create(HashAlgorithm::kSha256));
// Hash the RIFF header.
ABSL_RETURN_IF_ERROR(hasher->Update(source, 0, instructions[0].offset));
// Hash the updated RIFF size.
hasher->Update(instructions[0].bytes);
// Hash up to the C2PA chunk insertion point.
ABSL_RETURN_IF_ERROR(hasher->Update(source, 8, instructions[1].offset - 8));
// Hash the C2PA chunk placeholder.
hasher->Update(instructions[1].bytes);
// Hash the remainder of the file.
ABSL_RETURN_IF_ERROR(hasher->Update(source, instructions[1].offset,
file_size - instructions[1].offset));
return hasher->Digest();
}
} // namespace
absl::StatusOr<std::vector<ByteInstruction>> RiffWriter::WriteInternal(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
OutputMetadata* absl_nullable output_metadata) const {
if (request.has_hard_binding()) {
return absl::InvalidArgumentError(
"request must not include `hard_binding` (binding is computed by "
"RiffWriter)");
}
uint64_t file_size = source->Size().value_or(0);
if (file_size == 0) {
return absl::InvalidArgumentError("source is empty");
}
// The two-phase approach is required for RIFF: The box hash can't be computed
// correctly until the manifest store size is known, because the manifest size
// affects the top-level RIFF chunk size.
// Phase 1: Compute preliminary hard binding, produce a manifest store
// template, and embed a zero-filled placeholder in the asset.
ABSL_ASSIGN_OR_RETURN(*request.mutable_hard_binding()->mutable_data_hash(),
DataHashTemplate(HashAlgorithm::kSha256));
ABSL_ASSIGN_OR_RETURN(ManifestStoreTemplate manifest_store_template,
generator_.GenerateManifestStoreTemplate(request));
constexpr int64_t kMaxManifestStoreSizeBytes = 100 * 1024 * 1024; // 100 MiB
if (manifest_store_template.manifest_store_size() <= 0 ||
manifest_store_template.manifest_store_size() >
kMaxManifestStoreSizeBytes) {
return absl::InvalidArgumentError(
absl::StrCat("invalid manifest store template size: ",
manifest_store_template.manifest_store_size()));
}
// The insertion point is the end of the first RIFF chunk, or an error if the
// input is not a RIFF file or if a C2PA chunk already exists.
ABSL_ASSIGN_OR_RETURN(uint64_t c2pa_insertion_point,
FindC2paInsertionPoint(*source));
uint64_t manifest_store_size = manifest_store_template.manifest_store_size();
uint64_t pad_size = (manifest_store_size % 2 == 1) ? 1 : 0;
uint64_t updated_riff_size =
c2pa_insertion_point + manifest_store_size + pad_size;
if (updated_riff_size > std::numeric_limits<uint32_t>::max()) {
return absl::InvalidArgumentError("chunk size overflow");
}
std::vector<ByteInstruction> byte_instructions;
ABSL_RETURN_IF_ERROR(
AddRiffSizeInstruction(updated_riff_size, byte_instructions));
ABSL_RETURN_IF_ERROR(AddC2paManifestInstructionPlaceholder(
c2pa_insertion_point, manifest_store_size, pad_size, byte_instructions));
ABSL_ASSIGN_OR_RETURN(std::string hash,
ComputeHash(*source, file_size, byte_instructions));
FinalizeManifestStoreRequest finalize_request;
*finalize_request.mutable_manifest_store_template() =
std::move(manifest_store_template);
*finalize_request.mutable_hard_binding() = std::move(request.hard_binding());
finalize_request.mutable_hard_binding()
->mutable_data_hash()
->mutable_exclusions(0)
->set_start(c2pa_insertion_point + 8);
finalize_request.mutable_hard_binding()
->mutable_data_hash()
->mutable_exclusions(0)
->set_length(manifest_store_size);
finalize_request.mutable_hard_binding()->mutable_data_hash()->set_hash(
std::move(hash));
ABSL_ASSIGN_OR_RETURN(auto response,
generator_.FinalizeManifestStore(finalize_request));
if (output_metadata != nullptr) {
output_metadata->timestamp_included = response.is_timestamped();
}
if (response.manifest_store().size() !=
finalize_request.manifest_store_template().manifest_store_size()) {
return absl::InternalError("manifest store size does not match template");
}
// Update the C2PA chunk with the manifest store and padding.
byte_instructions.back().bytes = absl::StrCat(
byte_instructions.back().bytes.substr(0, 8), response.manifest_store(),
pad_size > 0 ? std::string(pad_size, '\0') : "");
return std::move(byte_instructions);
}
absl::Status RiffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata) const {
ABSL_ASSIGN_OR_RETURN(
std::vector<ByteInstruction> byte_instructions,
WriteInternal(std::move(request), source, output_metadata));
return ApplyByteInstructions(source, byte_instructions, destination);
}
absl::Status RiffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Writer* absl_nonnull asset,
OutputMetadata* absl_nullable output_metadata) const {
if (!asset->SupportsReadMode()) {
return absl::InvalidArgumentError("asset does not support read mode");
}
riegeli::Reader* source = asset->ReadMode(0);
if (source == nullptr) {
return asset->ok() ? absl::InternalError("failed to create source reader")
: asset->status();
}
if (!source->ok()) {
return source->status();
}
ABSL_ASSIGN_OR_RETURN(
std::vector<ByteInstruction> byte_instructions,
WriteInternal(std::move(request), source, output_metadata));
return ApplyByteInstructions(byte_instructions, asset);
}
} // namespace credentio