| // 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 |