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/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-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 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..96cce03c5 --- /dev/null +++ b/src/odr/internal/crypto/crypto_argon2.cpp @@ -0,0 +1,347 @@ +#include + +#include + +#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::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).data(), in.size()); + } +} + +void update32(CryptoPP::BLAKE2b &hash, const std::uint32_t value) { + std::array in; + util::byte::to_little_endian(value, in); + 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) { + const std::span bytes = bytes_of(in); + for (std::size_t i = 0; i < block_words; ++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'); + const std::span bytes(reinterpret_cast(out.data()), out.size()); + for (std::size_t i = 0; i < block_words; ++i) { + std::span word = bytes.subspan(8 * i); + util::byte::to_little_endian(block[i], word); + } + 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()); + } + + // 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) { + util::byte::to_little_endian(i, index_bytes); + 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");