diff --git a/src/archive.cpp b/src/archive.cpp index b27b340..4019575 100644 --- a/src/archive.cpp +++ b/src/archive.cpp @@ -15,7 +15,6 @@ namespace { constexpr char kArchiveMagic[kMagicSize] = {'M', '2', 'P', 'A', 'C', 'K', '2', '\0'}; -constexpr std::uint32_t kArchiveVersion = 1; constexpr std::size_t kCompressionMinSavingsBytes = 64; constexpr std::size_t kCompressionMinSavingsPercent = 5; @@ -46,6 +45,35 @@ Compression select_compression_mode( return Compression::Zstd; } +bool supports_archive_version(std::uint32_t version) +{ + return version == kArchiveVersionAead || version == kArchiveVersionStream; +} + +std::vector decrypt_payload_for_entry( + std::uint32_t version, + const std::vector& ciphertext, + const ManifestEntry& entry, + const std::array& master_key) +{ + if (version == kArchiveVersionAead) + { + return decrypt_payload(ciphertext, master_key, entry.nonce, entry.path); + } + + if (version == kArchiveVersionStream) + { + auto payload = decrypt_payload_stream(ciphertext, master_key, entry.nonce); + if (hash_bytes(payload) != entry.payload_hash) + { + fail("Payload hash mismatch: " + entry.path); + } + return payload; + } + + fail("Unsupported archive version"); +} + } // namespace std::string normalize_path(const std::filesystem::path& root, const std::filesystem::path& file) @@ -74,7 +102,7 @@ std::vector collect_files(const std::filesystem::path& ro return files; } -std::vector serialize_manifest(const std::vector& entries) +std::vector serialize_manifest(const std::vector& entries, std::uint32_t version) { std::vector bytes; const ManifestFixedHeader fixed { @@ -90,23 +118,45 @@ std::vector serialize_manifest(const std::vector& e fail("Path too long for manifest entry: " + entry.path); } - ManifestEntryFixed pod {}; - pod.path_size = static_cast(entry.path.size()); - pod.compression = static_cast(entry.compression); - pod.flags = 0; - pod.data_offset = entry.data_offset; - pod.original_size = entry.original_size; - pod.stored_size = entry.stored_size; - std::memcpy(pod.nonce, entry.nonce.data(), entry.nonce.size()); - std::memcpy(pod.plaintext_hash, entry.plaintext_hash.data(), entry.plaintext_hash.size()); - append_pod(bytes, pod); + if (version == kArchiveVersionAead) + { + ManifestEntryFixed pod {}; + pod.path_size = static_cast(entry.path.size()); + pod.compression = static_cast(entry.compression); + pod.flags = 0; + pod.data_offset = entry.data_offset; + pod.original_size = entry.original_size; + pod.stored_size = entry.stored_size; + std::memcpy(pod.nonce, entry.nonce.data(), entry.nonce.size()); + std::memcpy(pod.plaintext_hash, entry.plaintext_hash.data(), entry.plaintext_hash.size()); + append_pod(bytes, pod); + } + else if (version == kArchiveVersionStream) + { + ManifestEntryFixedV2 pod {}; + pod.path_size = static_cast(entry.path.size()); + pod.compression = static_cast(entry.compression); + pod.flags = 0; + pod.data_offset = entry.data_offset; + pod.original_size = entry.original_size; + pod.stored_size = entry.stored_size; + std::memcpy(pod.nonce, entry.nonce.data(), entry.nonce.size()); + std::memcpy(pod.payload_hash, entry.payload_hash.data(), entry.payload_hash.size()); + std::memcpy(pod.plaintext_hash, entry.plaintext_hash.data(), entry.plaintext_hash.size()); + append_pod(bytes, pod); + } + else + { + fail("Unsupported archive version"); + } + bytes.insert(bytes.end(), entry.path.begin(), entry.path.end()); } return bytes; } -std::vector parse_manifest(const std::vector& bytes) +std::vector parse_manifest(const std::vector& bytes, std::uint32_t version) { std::size_t offset = 0; const auto fixed = read_pod(bytes, offset); @@ -116,21 +166,44 @@ std::vector parse_manifest(const std::vector& bytes for (std::uint32_t i = 0; i < fixed.entry_count; ++i) { - const auto pod = read_pod(bytes, offset); - if (offset + pod.path_size > bytes.size()) + ManifestEntry entry; + std::uint16_t path_size = 0; + + if (version == kArchiveVersionAead) + { + const auto pod = read_pod(bytes, offset); + path_size = pod.path_size; + entry.compression = static_cast(pod.compression); + entry.data_offset = pod.data_offset; + entry.original_size = pod.original_size; + entry.stored_size = pod.stored_size; + std::memcpy(entry.nonce.data(), pod.nonce, entry.nonce.size()); + std::memcpy(entry.plaintext_hash.data(), pod.plaintext_hash, entry.plaintext_hash.size()); + } + else if (version == kArchiveVersionStream) + { + const auto pod = read_pod(bytes, offset); + path_size = pod.path_size; + entry.compression = static_cast(pod.compression); + entry.data_offset = pod.data_offset; + entry.original_size = pod.original_size; + entry.stored_size = pod.stored_size; + std::memcpy(entry.nonce.data(), pod.nonce, entry.nonce.size()); + std::memcpy(entry.payload_hash.data(), pod.payload_hash, entry.payload_hash.size()); + std::memcpy(entry.plaintext_hash.data(), pod.plaintext_hash, entry.plaintext_hash.size()); + } + else + { + fail("Unsupported archive version"); + } + + if (offset + path_size > bytes.size()) { fail("Manifest path data exceeds buffer"); } - ManifestEntry entry; - entry.path.assign(reinterpret_cast(bytes.data() + offset), pod.path_size); - offset += pod.path_size; - entry.compression = static_cast(pod.compression); - entry.data_offset = pod.data_offset; - entry.original_size = pod.original_size; - entry.stored_size = pod.stored_size; - std::memcpy(entry.nonce.data(), pod.nonce, entry.nonce.size()); - std::memcpy(entry.plaintext_hash.data(), pod.plaintext_hash, entry.plaintext_hash.size()); + entry.path.assign(reinterpret_cast(bytes.data() + offset), path_size); + offset += path_size; entries.push_back(std::move(entry)); } @@ -171,10 +244,23 @@ BuildResult build_archive( entry.data_offset = payload_bytes.size(); entry.original_size = plain.size(); entry.nonce = random_nonce(); + const auto& payload = entry.compression == Compression::Zstd ? compressed : plain; + entry.payload_hash = hash_bytes(payload); entry.plaintext_hash = hash_bytes(plain); - const auto& payload = entry.compression == Compression::Zstd ? compressed : plain; - const auto encrypted = encrypt_payload(payload, require_master_key(keys), entry.nonce, entry.path); + std::vector encrypted; + if (kCurrentArchiveVersion == kArchiveVersionAead) + { + encrypted = encrypt_payload(payload, require_master_key(keys), entry.nonce, entry.path); + } + else if (kCurrentArchiveVersion == kArchiveVersionStream) + { + encrypted = encrypt_payload_stream(payload, require_master_key(keys), entry.nonce); + } + else + { + fail("Unsupported archive version"); + } entry.stored_size = encrypted.size(); payload_bytes.insert(payload_bytes.end(), encrypted.begin(), encrypted.end()); @@ -185,13 +271,13 @@ BuildResult build_archive( result.total_stored_bytes += encrypted.size(); } - const auto manifest_bytes = serialize_manifest(manifest_entries); + const auto manifest_bytes = serialize_manifest(manifest_entries, kCurrentArchiveVersion); const auto manifest_hash = hash_bytes(manifest_bytes); const auto signature = sign_detached(manifest_bytes, *keys.signing_secret_key); ArchiveHeader header {}; std::memcpy(header.magic, kArchiveMagic, sizeof(kArchiveMagic)); - header.version = kArchiveVersion; + header.version = kCurrentArchiveVersion; header.flags = 0; header.key_id = keys.key_id; header.manifest_offset = sizeof(ArchiveHeader) + payload_bytes.size(); @@ -239,7 +325,7 @@ LoadedArchive load_archive(const std::filesystem::path& archive_path) { fail("Archive magic mismatch"); } - if (archive.header.version != kArchiveVersion) + if (!supports_archive_version(archive.header.version)) { fail("Unsupported archive version"); } @@ -252,7 +338,7 @@ LoadedArchive load_archive(const std::filesystem::path& archive_path) archive.file_bytes.begin() + static_cast(archive.header.manifest_offset), archive.file_bytes.begin() + static_cast(archive.header.manifest_offset + archive.header.manifest_size)); - archive.entries = parse_manifest(archive.manifest_bytes); + archive.entries = parse_manifest(archive.manifest_bytes, archive.header.version); return archive; } @@ -308,11 +394,14 @@ bool verify_archive( for (const auto& entry : archive.entries) { const auto plain = extract_entry(archive, entry, keys.master_key); - const auto hash = hash_bytes(plain); - if (hash != entry.plaintext_hash) + if (archive.header.version == kArchiveVersionAead || archive.header.version == kArchiveVersionStream) { - error = "Plaintext hash mismatch: " + entry.path; - return false; + const auto hash = hash_bytes(plain); + if (hash != entry.plaintext_hash) + { + error = "Plaintext hash mismatch: " + entry.path; + return false; + } } } } @@ -337,13 +426,13 @@ std::vector extract_entry( archive.file_bytes.begin() + static_cast(begin), archive.file_bytes.begin() + static_cast(end)); - const auto compressed = decrypt_payload(ciphertext, master_key, entry.nonce, entry.path); + const auto payload = decrypt_payload_for_entry(archive.header.version, ciphertext, entry, master_key); switch (entry.compression) { case Compression::None: - return compressed; + return payload; case Compression::Zstd: - return decompress_zstd(compressed, static_cast(entry.original_size)); + return decompress_zstd(payload, static_cast(entry.original_size)); default: fail("Unsupported compression mode"); } diff --git a/src/archive.h b/src/archive.h index e55354c..d4c4ffa 100644 --- a/src/archive.h +++ b/src/archive.h @@ -16,6 +16,9 @@ constexpr std::size_t kHashSize = 32; constexpr std::size_t kSignatureSize = 64; constexpr std::size_t kAeadKeySize = 32; constexpr std::size_t kAeadNonceSize = 24; +constexpr std::uint32_t kArchiveVersionAead = 1; +constexpr std::uint32_t kArchiveVersionStream = 2; +constexpr std::uint32_t kCurrentArchiveVersion = kArchiveVersionAead; enum class Compression : std::uint8_t { @@ -54,6 +57,19 @@ struct ManifestEntryFixed std::uint8_t nonce[kAeadNonceSize]; std::uint8_t plaintext_hash[kHashSize]; }; + +struct ManifestEntryFixedV2 +{ + std::uint16_t path_size; + std::uint8_t compression; + std::uint8_t flags; + std::uint64_t data_offset; + std::uint64_t original_size; + std::uint64_t stored_size; + std::uint8_t nonce[kAeadNonceSize]; + std::uint8_t payload_hash[kHashSize]; + std::uint8_t plaintext_hash[kHashSize]; +}; #pragma pack(pop) struct ManifestEntry @@ -64,6 +80,7 @@ struct ManifestEntry std::uint64_t original_size = 0; std::uint64_t stored_size = 0; std::array nonce {}; + std::array payload_hash {}; std::array plaintext_hash {}; }; @@ -116,7 +133,7 @@ void extract_all( const std::filesystem::path& output_dir, const std::array& master_key); -std::vector serialize_manifest(const std::vector& entries); -std::vector parse_manifest(const std::vector& bytes); +std::vector serialize_manifest(const std::vector& entries, std::uint32_t version); +std::vector parse_manifest(const std::vector& bytes, std::uint32_t version); } // namespace m2pack diff --git a/src/crypto.cpp b/src/crypto.cpp index f1364af..5a39508 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -205,6 +205,42 @@ std::vector decrypt_payload( return plaintext; } +std::vector encrypt_payload_stream( + const std::vector& plaintext, + const std::array& key, + const std::array& nonce) +{ + std::vector ciphertext(plaintext.size()); + if (!plaintext.empty()) + { + crypto_stream_xchacha20_xor( + ciphertext.data(), + plaintext.data(), + plaintext.size(), + nonce.data(), + key.data()); + } + return ciphertext; +} + +std::vector decrypt_payload_stream( + const std::vector& ciphertext, + const std::array& key, + const std::array& nonce) +{ + std::vector plaintext(ciphertext.size()); + if (!ciphertext.empty()) + { + crypto_stream_xchacha20_xor( + plaintext.data(), + ciphertext.data(), + ciphertext.size(), + nonce.data(), + key.data()); + } + return plaintext; +} + std::vector sign_detached( const std::vector& data, const std::vector& secret_key) diff --git a/src/crypto.h b/src/crypto.h index 2e131e1..e6bdb9e 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -38,6 +38,16 @@ std::vector decrypt_payload( const std::array& nonce, std::string_view associated_data); +std::vector encrypt_payload_stream( + const std::vector& plaintext, + const std::array& key, + const std::array& nonce); + +std::vector decrypt_payload_stream( + const std::vector& ciphertext, + const std::array& key, + const std::array& nonce); + std::vector sign_detached( const std::vector& data, const std::vector& secret_key);