From 6cb931c84279ebe10df3b3e5ab14f1fadaa2fe01 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 20:02:26 +0200 Subject: [PATCH 1/3] refactor(crypto): implement argon2id in-tree, drop the argon2 dependency The argon2 package was the last recipe we carried in conan-odr-index that ConanCenter does not have. It only existed because upstream's Makefile cannot cross-compile for Android, and ConanCenter declined the fix since upstream has been unmaintained since 2021. Argon2id on top of Crypto++'s BLAKE2b is ~300 lines, so implement it rather than swap in another crypto library: Crypto++ has no Argon2, libsodium hardcodes p=1 while LibreOffice writes p=4, and Botan/OpenSSL would be a second full crypto stack. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PZSnvAQFbn3MsD5tt5JGz3 --- CMakeLists.txt | 3 +- conan.lock | 3 +- conanfile.py | 1 - src/odr/internal/crypto/README.md | 32 ++ src/odr/internal/crypto/crypto_argon2.cpp | 357 ++++++++++++++++++ src/odr/internal/crypto/crypto_argon2.hpp | 17 + src/odr/internal/crypto/crypto_util.cpp | 10 +- test/src/internal/crypto/crypto_util_test.cpp | 37 ++ 8 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 src/odr/internal/crypto/README.md create mode 100644 src/odr/internal/crypto/crypto_argon2.cpp create mode 100644 src/odr/internal/crypto/crypto_argon2.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 93e0da4db..bf9161b6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,6 @@ find_package(nlohmann_json REQUIRED) find_package(vincentlaucsb-csv-parser REQUIRED) find_package(uchardet REQUIRED) find_package(utf8cpp REQUIRED) -find_package(argon2 REQUIRED) set(PRE_CONFIGURE_FILE "src/odr/internal/git_info.cpp.in") set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/src/odr/internal/git_info.cpp") @@ -124,6 +123,7 @@ set(ODR_SOURCE_FILES "src/odr/internal/common/table_range.cpp" "src/odr/internal/common/temporary_file.cpp" + "src/odr/internal/crypto/crypto_argon2.cpp" "src/odr/internal/crypto/crypto_util.cpp" "src/odr/internal/csv/csv_file.cpp" @@ -267,7 +267,6 @@ target_link_libraries(odr vincentlaucsb-csv-parser::vincentlaucsb-csv-parser uchardet::uchardet utf8::cpp - argon2::argon2 ) if (ODR_WITH_HTTP_SERVER) diff --git a/conan.lock b/conan.lock index 9447dfe67..687603fd7 100644 --- a/conan.lock +++ b/conan.lock @@ -14,8 +14,7 @@ "gtest/1.14.0#f8f0757a574a8dd747d16af62d6eb1b7%1743410807.169", "cryptopp/8.9.0#7a51e0038756b21bc3a6b82d681d5906%1758206597.119", "cpp-httplib/0.47.0#add6673ff352c26898ed2650453e706e%1784539639.401", - "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465", - "argon2/20190702-odr#965901884bc82ec8a7c0a1305d42c127%1784987057.981858" + "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465" ], "build_requires": [ "zstd/1.5.7#b68ca8e3de04ba5957761751d1d661f4%1760955092.069", diff --git a/conanfile.py b/conanfile.py index 67687c061..093fe9b23 100644 --- a/conanfile.py +++ b/conanfile.py @@ -55,7 +55,6 @@ def requirements(self): self.requires("utfcpp/4.0.9") if self.options.get_safe("with_http_server", False): self.requires("cpp-httplib/0.47.0") - self.requires("argon2/20190702-odr") if self.options.get_safe("with_python", False): self.requires("pybind11/2.13.6") diff --git a/src/odr/internal/crypto/README.md b/src/odr/internal/crypto/README.md new file mode 100644 index 000000000..0af808778 --- /dev/null +++ b/src/odr/internal/crypto/README.md @@ -0,0 +1,32 @@ +# Crypto implementation + +Thin wrappers over [Crypto++](https://www.cryptopp.com/) in `crypto_util.*`, +plus one algorithm Crypto++ does not ship: Argon2id. + +## Argon2id + +`crypto_argon2.*` implements Argon2id per [RFC 9106], version `0x13`, without +secret or associated data, using Crypto++ for BLAKE2b. Lanes are computed +sequentially — correct for any `p`, just not in parallel. + +It is used by [ODF](../odf/README.md) for LibreOffice's "wholesome" package +encryption (LibreOffice 24.8+, ODF 1.5), which writes `t=3`, `m=65536` KiB, +`p=4` lanes. + +### Why hand-rolled + +- Crypto++ has no Argon2, and has had no release since 8.9.0 (2023). +- The reference implementation, [P-H-C/phc-winner-argon2], is unmaintained + since 2021 and its Makefile cannot cross-compile for Android. ConanCenter + [declined to carry the fix][cci-pr] because upstream would never merge it, so + depending on it meant maintaining our own Conan recipe for one function. + Upstream PR: [#392]. +- libsodium hardcodes `p=1`, so it cannot read the files above. +- Botan and OpenSSL both work, but mean a second full crypto library. + +Tests cross-check against the reference implementation's published vectors. + +[RFC 9106]: https://www.rfc-editor.org/rfc/rfc9106 +[P-H-C/phc-winner-argon2]: https://github.com/P-H-C/phc-winner-argon2 +[cci-pr]: https://github.com/conan-io/conan-center-index/pull/27800 +[#392]: https://github.com/P-H-C/phc-winner-argon2/pull/392 diff --git a/src/odr/internal/crypto/crypto_argon2.cpp b/src/odr/internal/crypto/crypto_argon2.cpp new file mode 100644 index 000000000..1fa02f406 --- /dev/null +++ b/src/odr/internal/crypto/crypto_argon2.cpp @@ -0,0 +1,357 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace odr::internal::crypto { + +namespace { + +using byte = std::uint8_t; + +constexpr std::uint32_t argon2_version = 0x13; +constexpr std::uint32_t argon2id_type = 2; +constexpr std::size_t block_size = 1024; +constexpr std::size_t block_words = block_size / 8; +constexpr std::uint32_t slices = 4; ///< synchronisation points per pass +constexpr std::uint32_t prehash_size = 64; +constexpr std::uint32_t max_uint32 = std::numeric_limits::max(); + +/// One 1024 byte Argon2 block, as the 128 little-endian words it is mixed as. +using Block = std::array; + +std::uint64_t load64(const byte *const in) { + std::uint64_t value = 0; + for (std::size_t i = 0; i < 8; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +void store32(byte *const out, const std::uint32_t value) { + for (std::size_t i = 0; i < 4; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +void store64(byte *const out, const std::uint64_t value) { + for (std::size_t i = 0; i < 8; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +const byte *bytes_of(const std::string_view in) { + return reinterpret_cast(in.data()); +} + +void update(CryptoPP::BLAKE2b &hash, const std::string_view in) { + if (!in.empty()) { + hash.Update(bytes_of(in), in.size()); + } +} + +void update32(CryptoPP::BLAKE2b &hash, const std::uint32_t value) { + std::array in; + store32(in.data(), value); + hash.Update(in.data(), in.size()); +} + +/// The variable-length hash H' from [RFC 9106] 3.3. +std::string blake2b_long(const std::size_t out_size, + const std::string_view in) { + std::string out(out_size, '\0'); + auto *const data = reinterpret_cast(out.data()); + + if (out_size <= prehash_size) { + CryptoPP::BLAKE2b hash(static_cast(out_size)); + update32(hash, static_cast(out_size)); + update(hash, in); + hash.Final(data); + return out; + } + + // Beyond 64 bytes the output is the first half of a chain of 64 byte + // digests, with the tail of the last one filling the remainder. + std::array buffer; + { + CryptoPP::BLAKE2b hash(static_cast(prehash_size)); + update32(hash, static_cast(out_size)); + update(hash, in); + hash.Final(buffer.data()); + } + std::ranges::copy_n(buffer.begin(), 32, data); + + std::size_t written = 32; + while (out_size - written > prehash_size) { + CryptoPP::BLAKE2b hash(static_cast(prehash_size)); + hash.Update(buffer.data(), buffer.size()); + hash.Final(buffer.data()); + std::ranges::copy_n(buffer.begin(), 32, data + written); + written += 32; + } + + CryptoPP::BLAKE2b hash(static_cast(out_size - written)); + hash.Update(buffer.data(), buffer.size()); + hash.Final(data + written); + return out; +} + +void load_block(Block &block, const std::string_view in) { + for (std::size_t i = 0; i < block_words; ++i) { + block[i] = load64(bytes_of(in) + 8 * i); + } +} + +std::string store_block(const Block &block) { + std::string out(block_size, '\0'); + auto *const data = reinterpret_cast(out.data()); + for (std::size_t i = 0; i < block_words; ++i) { + store64(data + 8 * i, block[i]); + } + return out; +} + +std::uint64_t bla_mka(const std::uint64_t x, const std::uint64_t y) { + return x + y + 2 * (x & max_uint32) * (y & max_uint32); +} + +void mix(std::uint64_t &a, std::uint64_t &b, std::uint64_t &c, + std::uint64_t &d) { + a = bla_mka(a, b); + d = std::rotr(d ^ a, 32); + c = bla_mka(c, d); + b = std::rotr(b ^ c, 24); + a = bla_mka(a, b); + d = std::rotr(d ^ a, 16); + c = bla_mka(c, d); + b = std::rotr(b ^ c, 63); +} + +/// The permutation P from [RFC 9106] 3.6, over the 16 words of `block` picked +/// out by `at`. +void permute(Block &block, const std::array &at) { + mix(block[at[0]], block[at[4]], block[at[8]], block[at[12]]); + mix(block[at[1]], block[at[5]], block[at[9]], block[at[13]]); + mix(block[at[2]], block[at[6]], block[at[10]], block[at[14]]); + mix(block[at[3]], block[at[7]], block[at[11]], block[at[15]]); + mix(block[at[0]], block[at[5]], block[at[10]], block[at[15]]); + mix(block[at[1]], block[at[6]], block[at[11]], block[at[12]]); + mix(block[at[2]], block[at[7]], block[at[8]], block[at[13]]); + mix(block[at[3]], block[at[4]], block[at[9]], block[at[14]]); +} + +/// The compression function G from [RFC 9106] 3.5. `accumulate` selects the +/// later passes, which XOR into `next` instead of overwriting it. +void compress(const Block &previous, const Block &reference, Block &next, + const bool accumulate) { + Block r; + for (std::size_t i = 0; i < block_words; ++i) { + r[i] = previous[i] ^ reference[i]; + } + Block sum = r; + if (accumulate) { + for (std::size_t i = 0; i < block_words; ++i) { + sum[i] ^= next[i]; + } + } + + for (std::size_t row = 0; row < 8; ++row) { + std::array at; + for (std::size_t i = 0; i < at.size(); ++i) { + at[i] = 16 * row + i; + } + permute(r, at); + } + for (std::size_t column = 0; column < 8; ++column) { + std::array at; + for (std::size_t i = 0; i < at.size(); ++i) { + at[i] = 2 * column + 16 * (i / 2) + i % 2; + } + permute(r, at); + } + + for (std::size_t i = 0; i < block_words; ++i) { + next[i] = sum[i] ^ r[i]; + } +} + +/// Refills `addresses` with the next 128 data-independent selectors. +void next_addresses(Block &addresses, Block &input) { + constexpr Block zero{}; + ++input[6]; + compress(zero, input, addresses, false); + compress(zero, addresses, addresses, false); +} + +/// Maps a selector onto a block of the reference lane, [RFC 9106] 3.4.1.2. +std::uint32_t +reference_index(const std::uint32_t pass, const std::uint32_t slice, + const std::uint32_t index, const std::uint32_t segment_length, + const std::uint32_t lane_length, const bool same_lane, + const std::uint64_t selector) { + std::uint32_t area; + if (pass == 0) { + if (slice == 0) { + area = index - 1; + } else if (same_lane) { + area = slice * segment_length + index - 1; + } else { + area = slice * segment_length - (index == 0 ? 1 : 0); + } + } else if (same_lane) { + area = lane_length - segment_length + index - 1; + } else { + area = lane_length - segment_length - (index == 0 ? 1 : 0); + } + + // A quadratic distribution over the area, biased towards recent blocks. + const std::uint64_t low = selector & max_uint32; + const std::uint64_t square = (low * low) >> 32; + const std::uint64_t relative = + static_cast(area) - 1 - ((area * square) >> 32); + + const std::uint32_t start = + (pass == 0 || slice == slices - 1) ? 0 : (slice + 1) * segment_length; + return static_cast((start + relative) % lane_length); +} + +} // namespace + +std::string argon2::id(const std::size_t tag_size, + const std::string_view password, + const std::string_view salt, + const std::size_t iterations, const std::size_t memory, + const std::size_t lanes) { + if (tag_size < 4 || tag_size > max_uint32) { + throw std::invalid_argument("argon2id: tag size out of range"); + } + if (salt.size() < 8) { + throw std::invalid_argument("argon2id: salt too short"); + } + if (iterations < 1 || iterations > max_uint32) { + throw std::invalid_argument("argon2id: iterations out of range"); + } + if (lanes < 1 || lanes > 0xffffff) { + throw std::invalid_argument("argon2id: lanes out of range"); + } + if (memory < 8 * lanes || memory > max_uint32) { + throw std::invalid_argument("argon2id: memory out of range"); + } + + const auto passes = static_cast(iterations); + const auto lane_count = static_cast(lanes); + const std::uint32_t segment_length = + static_cast(memory) / (lane_count * slices); + const std::uint32_t lane_length = segment_length * slices; + const std::uint32_t block_count = lane_length * lane_count; + + std::array prehash; + { + CryptoPP::BLAKE2b hash(static_cast(prehash_size)); + update32(hash, lane_count); + update32(hash, static_cast(tag_size)); + update32(hash, static_cast(memory)); + update32(hash, passes); + update32(hash, argon2_version); + update32(hash, argon2id_type); + update32(hash, static_cast(password.size())); + update(hash, password); + update32(hash, static_cast(salt.size())); + update(hash, salt); + update32(hash, 0); // secret + update32(hash, 0); // associated data + hash.Final(prehash.data()); + } + + std::vector blocks(block_count); + for (std::uint32_t lane = 0; lane < lane_count; ++lane) { + const std::size_t offset = static_cast(lane) * lane_length; + for (std::uint32_t i = 0; i < 2; ++i) { + store32(prehash.data() + prehash_size, i); + store32(prehash.data() + prehash_size + 4, lane); + load_block(blocks[offset + i], + blake2b_long(block_size, + {reinterpret_cast(prehash.data()), + prehash.size()})); + } + } + + Block address_input; + Block addresses; + for (std::uint32_t pass = 0; pass < passes; ++pass) { + for (std::uint32_t slice = 0; slice < slices; ++slice) { + for (std::uint32_t lane = 0; lane < lane_count; ++lane) { + // Argon2id indexes the first half of the first pass without looking at + // the data, and everything after it like Argon2d. + const bool independent = pass == 0 && slice < slices / 2; + if (independent) { + address_input = {}; + address_input[0] = pass; + address_input[1] = lane; + address_input[2] = slice; + address_input[3] = block_count; + address_input[4] = passes; + address_input[5] = argon2id_type; + } + + std::uint32_t index = 0; + if (pass == 0 && slice == 0) { + index = 2; // the first two blocks of the lane are already filled + if (independent) { + next_addresses(addresses, address_input); + } + } + + for (; index < segment_length; ++index) { + const std::uint32_t current = + lane * lane_length + slice * segment_length + index; + const std::uint32_t previous = current % lane_length == 0 + ? current + lane_length - 1 + : current - 1; + + std::uint64_t selector; + if (independent) { + if (index % block_words == 0) { + next_addresses(addresses, address_input); + } + selector = addresses[index % block_words]; + } else { + selector = blocks[previous][0]; + } + + // The first slice of the first pass has no other lane to point at. + const std::uint32_t reference_lane = + pass == 0 && slice == 0 + ? lane + : static_cast((selector >> 32) % lane_count); + const std::uint32_t reference = + reference_lane * lane_length + + reference_index(pass, slice, index, segment_length, lane_length, + reference_lane == lane, selector); + + compress(blocks[previous], blocks[reference], blocks[current], + pass != 0); + } + } + } + } + + Block result = blocks[lane_length - 1]; + for (std::uint32_t lane = 1; lane < lane_count; ++lane) { + const std::size_t offset = static_cast(lane) * lane_length; + const Block &last = blocks[offset + lane_length - 1]; + for (std::size_t i = 0; i < block_words; ++i) { + result[i] ^= last[i]; + } + } + return blake2b_long(tag_size, store_block(result)); +} + +} // namespace odr::internal::crypto diff --git a/src/odr/internal/crypto/crypto_argon2.hpp b/src/odr/internal/crypto/crypto_argon2.hpp new file mode 100644 index 000000000..981517451 --- /dev/null +++ b/src/odr/internal/crypto/crypto_argon2.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace odr::internal::crypto::argon2 { + +/// Argon2id [RFC 9106], version 0x13, without secret or associated data. +/// `memory` is in KiB and is rounded down to a multiple of `4 * lanes`. Lanes +/// are computed sequentially. Throws `std::invalid_argument` for parameters +/// outside the ranges the spec allows. +std::string id(std::size_t tag_size, std::string_view password, + std::string_view salt, std::size_t iterations, + std::size_t memory, std::size_t lanes); + +} // namespace odr::internal::crypto::argon2 diff --git a/src/odr/internal/crypto/crypto_util.cpp b/src/odr/internal/crypto/crypto_util.cpp index 33838aaaa..013da24a8 100644 --- a/src/odr/internal/crypto/crypto_util.cpp +++ b/src/odr/internal/crypto/crypto_util.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -25,8 +27,6 @@ #include #include -#include - namespace odr::internal::crypto { using byte = std::uint8_t; @@ -144,11 +144,7 @@ std::string util::argon2id(const std::size_t key_size, const std::string &salt, const std::size_t iteration_count, const std::size_t memory, const std::size_t lanes) { - std::string result(key_size, '\0'); - argon2id_hash_raw(iteration_count, memory, lanes, start_key.data(), - start_key.size(), salt.data(), salt.size(), result.data(), - result.size()); - return result; + return argon2::id(key_size, start_key, salt, iteration_count, memory, lanes); } std::string util::decrypt_aes_ecb(const std::string &key, diff --git a/test/src/internal/crypto/crypto_util_test.cpp b/test/src/internal/crypto/crypto_util_test.cpp index fa70567bc..f5ecae2dd 100644 --- a/test/src/internal/crypto/crypto_util_test.cpp +++ b/test/src/internal/crypto/crypto_util_test.cpp @@ -42,6 +42,43 @@ TEST(CryptoUtil, hex_decode_rejects_bad_input) { EXPECT_THROW(hex_decode("48 65"), std::invalid_argument); } +// Argon2id v=19 vectors published by the reference implementation +// (P-H-C/phc-winner-argon2, src/test.c). +TEST(CryptoUtil, argon2id) { + EXPECT_EQ(hex_encode(argon2id(32, "password", "somesalt", 2, 65536, 1)), + "09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7"); + EXPECT_EQ(hex_encode(argon2id(32, "password", "somesalt", 2, 256, 1)), + "9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe"); + EXPECT_EQ(hex_encode(argon2id(32, "password", "somesalt", 2, 256, 2)), + "6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037"); +} + +// Generated with the same reference implementation — its published vectors go +// no further than two lanes. +TEST(CryptoUtil, argon2id_lanes) { + // What LibreOffice writes for ODF package encryption. + EXPECT_EQ( + hex_encode(argon2id(32, "password", "0123456789abcdef", 3, 65536, 4)), + "b8a64b68dea6b88ca8c8862be706aac37cbecda0db7bd68b48f8fa2e7feb6f3e"); + EXPECT_EQ(hex_encode(argon2id(64, "password", "somesalt", 2, 256, 4)), + "862f0a0272a6ce8aeb7edf3efabd8287b7dfa4c550207c77471532fec400e46e" + "a5751a3a8fe4cb2ede8b60ca66de2a8180d3ea39a242d0b4e413f834b9a049ad"); + // Memory is rounded down to a multiple of 4 * lanes. + EXPECT_EQ(hex_encode(argon2id(32, "password", "somesalt", 2, 37, 3)), + "fd3d6c0350c90b38be1da55d3387c3da995b683542cf1de6af4cb06f0cbd1188"); +} + +TEST(CryptoUtil, argon2id_rejects_bad_parameters) { + EXPECT_THROW(argon2id(32, "password", "short", 2, 256, 1), + std::invalid_argument); + EXPECT_THROW(argon2id(32, "password", "somesalt", 2, 32, 8), + std::invalid_argument); + EXPECT_THROW(argon2id(32, "password", "somesalt", 0, 256, 1), + std::invalid_argument); + EXPECT_THROW(argon2id(2, "password", "somesalt", 2, 256, 1), + std::invalid_argument); +} + // Well-known RC4 test vector (key "Key", plaintext "Plaintext"). TEST(CryptoUtil, rc4) { const std::string cipher = rc4("Key", "Plaintext"); From 3ade8713ff1c9d3396ec33c8d6d33411098352a2 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 20:12:55 +0200 Subject: [PATCH 2/3] build: drop the conan-odr-index submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit argon2 was the last recipe this repo pulled from it; everything else resolves from ConanCenter. CI no longer checks the submodule out or exports it, and the conan cache keys lose the submodule sha. The index itself stays where it is — other repos still use it, and its odrcore recipe still serves released 5.x. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PZSnvAQFbn3MsD5tt5JGz3 --- .github/workflows/android.yml | 6 +----- .github/workflows/apple.yml | 6 +----- .github/workflows/build_test.yml | 14 +++----------- .github/workflows/conan.yml | 4 ---- .github/workflows/python.yml | 10 +++------- .github/workflows/tidy.yml | 4 ---- .gitmodules | 3 --- conan-odr-index | 1 - 8 files changed, 8 insertions(+), 40 deletions(-) delete mode 100644 .gitmodules delete mode 160000 conan-odr-index diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2d09b06fd..dc9ca4119 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -41,8 +41,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - name: install ccache run: | @@ -61,7 +59,7 @@ jobs: - name: conan cache key shell: bash - run: echo "CONAN_CACHE_KEY=$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" + run: echo "CONAN_CACHE_KEY=${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" - name: cache conan uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 @@ -71,8 +69,6 @@ jobs: restore-keys: | conan-${{ env.CACHE_FLAVOR }}-android-${{ matrix.architecture }}-${{ env.CONAN_KEY_SUFFIX }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index fc828aa28..c7285ebc3 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -74,8 +74,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index # `create-xcframework` behaviour and the default deployment targets move # between Xcode versions, so the runner default is not good enough. @@ -98,7 +96,7 @@ jobs: - name: conan cache key shell: bash - run: echo "CONAN_CACHE_KEY=$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" + run: echo "CONAN_CACHE_KEY=${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" - name: cache conan uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 @@ -108,8 +106,6 @@ jobs: restore-keys: | conan-${{ env.CACHE_FLAVOR }}-${{ matrix.profile }}-${{ env.CONAN_KEY_SUFFIX }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 1eac5f0ce..1474d872d 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -54,8 +54,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - name: ubuntu install ccache if: runner.os == 'Linux' @@ -84,7 +82,7 @@ jobs: - name: conan cache key shell: bash - run: echo "CONAN_CACHE_KEY=$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" + run: echo "CONAN_CACHE_KEY=${{ hashFiles('conanfile.py', '.github/config/conan/**') }}" >> "$GITHUB_ENV" - name: cache conan uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 @@ -94,8 +92,6 @@ jobs: restore-keys: | conan-${{ env.CACHE_FLAVOR }}-${{ matrix.host_profile }}-${{ env.CONAN_KEY_SUFFIX }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan @@ -328,7 +324,7 @@ jobs: build-test-downstream: runs-on: ${{ matrix.os }} - # Exports the full odr-index (no `--selection-config`) and resolves through + # Exports odrcore and builds a consumer against it, resolving through # `conan.lock`, so its dependency set differs from the `build` job's. env: CACHE_FLAVOR: downstream @@ -340,8 +336,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - name: ubuntu install ccache if: runner.os == 'Linux' @@ -363,7 +357,7 @@ jobs: - name: conan cache key shell: bash - run: echo "CONAN_CACHE_KEY=$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conanfile.py', 'conan.lock', '.github/config/conan/**') }}" >> "$GITHUB_ENV" + run: echo "CONAN_CACHE_KEY=${{ hashFiles('conanfile.py', 'conan.lock', '.github/config/conan/**') }}" >> "$GITHUB_ENV" - name: cache conan uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 @@ -373,8 +367,6 @@ jobs: restore-keys: | conan-${{ env.CACHE_FLAVOR }}-${{ matrix.host_profile }}-${{ env.CONAN_KEY_SUFFIX }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py - name: conan config run: conan config install .github/config/conan diff --git a/.github/workflows/conan.yml b/.github/workflows/conan.yml index b568fc25d..26f5d0832 100644 --- a/.github/workflows/conan.yml +++ b/.github/workflows/conan.yml @@ -22,8 +22,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - name: ubuntu install ccache if: runner.os == 'Linux' @@ -54,8 +52,6 @@ jobs: restore-keys: | conan-${{ matrix.host_profile }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d3aee263b..16d737aa1 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -61,8 +61,6 @@ jobs: with: # setuptools-scm derives the package version from git tags. fetch-depth: 0 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index # The conan profiles use ccache as compiler launcher, so it must exist # even for `--build missing` source builds. @@ -86,7 +84,7 @@ jobs: - name: conan cache key shell: bash - run: echo "CONAN_CACHE_KEY=$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conanfile.py', 'pyproject.toml', '.github/config/conan/**') }}" >> "$GITHUB_ENV" + run: echo "CONAN_CACHE_KEY=${{ hashFiles('conanfile.py', 'pyproject.toml', '.github/config/conan/**') }}" >> "$GITHUB_ENV" - name: cache conan uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 @@ -96,8 +94,6 @@ jobs: restore-keys: | conan-${{ env.CACHE_FLAVOR }}-${{ matrix.host_profile }}-${{ env.CONAN_KEY_SUFFIX }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan @@ -195,8 +191,8 @@ jobs: # Uses PyPI trusted publishing (OIDC, no token): the `pyodr` project on PyPI # must list this repo + workflow + the `pypi` environment as a publisher. # Note pip cannot build the published sdist on its own (the build needs a - # conan-generated toolchain plus the conan-odr-index recipes, see - # python/README.md); it is published for completeness. + # conan-generated toolchain, see python/README.md); it is published for + # completeness. pypi: needs: [wheels, sdist] runs-on: ubuntu-24.04 diff --git a/.github/workflows/tidy.yml b/.github/workflows/tidy.yml index fe53042ba..c4108b7cb 100644 --- a/.github/workflows/tidy.yml +++ b/.github/workflows/tidy.yml @@ -29,8 +29,6 @@ jobs: steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - name: ubuntu install ccache if: runner.os == 'Linux' @@ -60,8 +58,6 @@ jobs: restore-keys: | conan-${{ matrix.host_profile }}- - - name: export conan-odr-index - run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml - name: conan config run: conan config install .github/config/conan diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 97d2b5bd3..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "conan-odr-index"] - path = conan-odr-index - url = https://github.com/opendocument-app/conan-odr-index.git diff --git a/conan-odr-index b/conan-odr-index deleted file mode 160000 index e5034c4bf..000000000 --- a/conan-odr-index +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e5034c4bf2a15ae7a19910ee2c1a8be8a0935aaf From 628d21e302f2ef3bdc9c1ed6d79f879148699149 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 20:36:03 +0200 Subject: [PATCH 3/3] refactor(crypto): use util::byte for argon2 word (de)serialisation Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PZSnvAQFbn3MsD5tt5JGz3 --- src/odr/internal/crypto/crypto_argon2.cpp | 48 +++++++++-------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/src/odr/internal/crypto/crypto_argon2.cpp b/src/odr/internal/crypto/crypto_argon2.cpp index 1fa02f406..96cce03c5 100644 --- a/src/odr/internal/crypto/crypto_argon2.cpp +++ b/src/odr/internal/crypto/crypto_argon2.cpp @@ -1,10 +1,13 @@ #include +#include + #include #include #include #include #include +#include #include #include @@ -27,39 +30,19 @@ constexpr std::uint32_t max_uint32 = std::numeric_limits::max(); /// One 1024 byte Argon2 block, as the 128 little-endian words it is mixed as. using Block = std::array; -std::uint64_t load64(const byte *const in) { - std::uint64_t value = 0; - for (std::size_t i = 0; i < 8; ++i) { - value |= static_cast(in[i]) << (8 * i); - } - return value; -} - -void store32(byte *const out, const std::uint32_t value) { - for (std::size_t i = 0; i < 4; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -void store64(byte *const out, const std::uint64_t value) { - for (std::size_t i = 0; i < 8; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -const byte *bytes_of(const std::string_view in) { - return reinterpret_cast(in.data()); +std::span bytes_of(const std::string_view in) { + return {reinterpret_cast(in.data()), in.size()}; } void update(CryptoPP::BLAKE2b &hash, const std::string_view in) { if (!in.empty()) { - hash.Update(bytes_of(in), in.size()); + hash.Update(bytes_of(in).data(), in.size()); } } void update32(CryptoPP::BLAKE2b &hash, const std::uint32_t value) { std::array in; - store32(in.data(), value); + util::byte::to_little_endian(value, in); hash.Update(in.data(), in.size()); } @@ -104,16 +87,19 @@ std::string blake2b_long(const std::size_t out_size, } void load_block(Block &block, const std::string_view in) { + const std::span bytes = bytes_of(in); for (std::size_t i = 0; i < block_words; ++i) { - block[i] = load64(bytes_of(in) + 8 * i); + block[i] = + util::byte::from_little_endian(bytes.subspan(8 * i)); } } std::string store_block(const Block &block) { std::string out(block_size, '\0'); - auto *const data = reinterpret_cast(out.data()); + const std::span bytes(reinterpret_cast(out.data()), out.size()); for (std::size_t i = 0; i < block_words; ++i) { - store64(data + 8 * i, block[i]); + std::span word = bytes.subspan(8 * i); + util::byte::to_little_endian(block[i], word); } return out; } @@ -270,12 +256,16 @@ std::string argon2::id(const std::size_t tag_size, hash.Final(prehash.data()); } + // H0 is followed by the block index and the lane, which vary per block. + std::span index_bytes = std::span(prehash).subspan(prehash_size, 4); + std::span lane_bytes = std::span(prehash).subspan(prehash_size + 4, 4); + std::vector blocks(block_count); for (std::uint32_t lane = 0; lane < lane_count; ++lane) { const std::size_t offset = static_cast(lane) * lane_length; + util::byte::to_little_endian(lane, lane_bytes); for (std::uint32_t i = 0; i < 2; ++i) { - store32(prehash.data() + prehash_size, i); - store32(prehash.data() + prehash_size + 4, lane); + util::byte::to_little_endian(i, index_bytes); load_block(blocks[offset + i], blake2b_long(block_size, {reinterpret_cast(prehash.data()),