From c2d7931b5c8b9630ce99d86b8fe96c6b6a928f87 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 15 Jul 2026 16:05:54 -0700 Subject: [PATCH] crypto: add generic SipHash-1-3-UJ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `SipHasher13UJ`, a block-oriented implementation accepting arbitrarily mixed 64-bit normal blocks and 256-bit jumbo blocks. For hash-table use, cryptographic hash outputs must make up all but a small bounded number of retained jumbo blocks. The generic interface serves as the executable specification for the fixed-width overloads added next. Co-authored-by: Lőrinc Co-authored-by: Jean-Philippe Aumasson --- src/crypto/siphash.cpp | 17 +++++++++ src/crypto/siphash.h | 86 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp index 4c3f3b9db5e..333fad275a6 100644 --- a/src/crypto/siphash.cpp +++ b/src/crypto/siphash.cpp @@ -49,6 +49,23 @@ uint64_t CSipHasher::Finalize() const .Finalize4(); } +SipHasher13UJ& SipHasher13UJ::Write(uint64_t data) noexcept +{ + m_state.Compress1(data); + return *this; +} + +SipHasher13UJ& SipHasher13UJ::WriteJumbo(const uint256& hash) noexcept +{ + m_state.Compress1Jumbo(hash); + return *this; +} + +uint64_t SipHasher13UJ::Finalize() const noexcept +{ + return m_state.Copy().Finalize3U(); +} + uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept { return m_state.Copy() diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h index 5bcc6d45536..0d17726c6cf 100644 --- a/src/crypto/siphash.h +++ b/src/crypto/siphash.h @@ -20,6 +20,9 @@ class SipHashState static constexpr uint64_t C0{0x736f6d6570736575}, C1{0x646f72616e646f6d}, C2{0x6c7967656e657261}, C3{0x7465646279746573}; /** SipHash v2 finalizer constant. */ static constexpr uint64_t FINALIZER{0xFF}; + /** SipHash custom unpadded finalizer constant. */ + static constexpr uint64_t FINALIZER_UNPADDED{0x6465646461706e75}; + static_assert(FINALIZER_UNPADDED != FINALIZER); /** Construct a SipHashState with the specified values as state. */ ALWAYS_INLINE SipHashState(uint64_t v0, uint64_t v1, uint64_t v2, uint64_t v3) noexcept : m_v0{v0}, m_v1{v1}, m_v2{v2}, m_v3{v3} {} @@ -43,6 +46,23 @@ public: explicit ALWAYS_INLINE SipHashState(uint64_t k0, uint64_t k1) noexcept : SipHashState{C0 ^ k0, C1 ^ k1, C2 ^ k0, C3 ^ k1} {} /** Construct a copy of this state. */ ALWAYS_INLINE SipHashState Copy() const noexcept { return {m_v0, m_v1, m_v2, m_v3}; } + /** Mutably compress one block into this state, with 1 SipRound. */ + ALWAYS_INLINE SipHashState& Compress1(uint64_t data) noexcept + { + m_v3 ^= data; + SipRound(); + m_v0 ^= data; + return *this; + } + /** Mutably compress one jumbo block into this state, with 1 SipRound. */ + ALWAYS_INLINE SipHashState& Compress1Jumbo(const uint256& data) noexcept + { + const uint64_t d0{data.GetUint64(0)}, d1{data.GetUint64(1)}, d2{data.GetUint64(2)}, d3{data.GetUint64(3)}; + m_v3 ^= d0; m_v0 ^= d1; m_v1 ^= d2; m_v2 ^= d3; + SipRound(); + m_v0 ^= d0; m_v1 ^= d1; m_v2 ^= d2; m_v3 ^= d3; + return *this; + } /** Mutably compress one block into this state, with 2 SipRounds. */ ALWAYS_INLINE SipHashState& Compress2(uint64_t data) noexcept { @@ -62,6 +82,16 @@ public: SipRound(); return m_v0 ^ m_v1 ^ m_v2 ^ m_v3; } + /** Mutably finalize this state with 3 SipRounds using the unpadded finalizer, and return the + * resulting hash. */ + ALWAYS_INLINE uint64_t Finalize3U() noexcept + { + m_v2 ^= FINALIZER_UNPADDED; + SipRound(); + SipRound(); + SipRound(); + return m_v0 ^ m_v1 ^ m_v2 ^ m_v3; + } }; /** General SipHash-2-4 implementation. */ @@ -85,6 +115,62 @@ public: uint64_t Finalize() const; }; +/** A custom weaker variant of SipHash-1-3 without padding, and supporting "jumbo" inputs. + * + * Compared to the traditional SipHash-2-4, this incorporates 3 changes: + * + * - Use SipHash-1-3: This common variant reduces the number of rounds between input blocks from 2 + * to 1, and the number of finalization rounds at the end from 4 to 3. + * It is a standard faster choice with weaker security guarantees. It is + * considered strong enough for use in hash tables and other applications with + * only weak security requirements, in particular when attackers cannot directly + * observe the hash function output, and cannot control k0 and k1. + * + * - Remove padding: Standard SipHash operates on byte-oriented inputs, which are converted into + * 8-byte blocks internally by adding a padding of 1-8 bytes to obtain a multiple + * of 8 bytes. In this variant, the input is a sequence of blocks instead, which + * are used without padding. This saves 1 round for inputs that are a multiple of + * 8 bytes already. The padding normally adds a commitment to the input's byte + * length, but since our input is block oriented, and there is one round per + * input, the function of that commitment is directly fulfilled by the number of + * rounds. The empty input is permitted; it yields the result of finalizing the + * initialization directly. Out of an abundance of caution the finalizing + * v2 ^= 0xff is changed to v2 ^= 0x6465646461706e75 ("unpadded" in LE64), to + * avoid collisions between standard SipHash-1-3 and this variant. + * + * - Jumbo blocks: We generalize the input to consist of a sequence of blocks which are each either + * exactly 8 bytes (normal blocks) or 32 bytes (jumbo blocks), and may be + * arbitrarily mixed. For hash-table use, cryptographic hash outputs must make up + * all but a small bounded number of retained jumbo blocks. Note that the block + * structure matters: hashing one jumbo block is not the same as hashing the same + * data split up into four normal blocks. Jumbo blocks are interpreted as 4 LE64 integers + * (d0,d1,d2,d3), and XOR'ed into the state before a round as + * (v0,v1,v2,v3) ^= (d1,d2,d3,d0), and XOR'ed into the state after a round as + * (v0,v1,v2,v3) ^= (d0,d1,d2,d3). This is a strict generalization of normal block + * processing, in the sense that if d1..d3=0, it is identical to processing a normal + * block d0 (a single LE64 integer). Of course, making d1..d3=0 should be impossible + * in the output of a cryptographic hash function. These jumbo blocks are acceptable + * because even though they may give the attacker more control within a single + * round, that control is limited by the cryptographic hash in between. + * + * Other components are unchanged: initialization constants, SipRound, and 64-bit output. + * This interface serves as an executable specification for fixed-width implementations. + */ +class SipHasher13UJ +{ + SipHashState m_state; + +public: + /** Construct a SipHash-1-3-UJ calculator initialized with 128-bit key (k0, k1). */ + SipHasher13UJ(uint64_t k0, uint64_t k1) noexcept : m_state{k0, k1} {} + /** Hash a normal 64-bit value. */ + SipHasher13UJ& Write(uint64_t data) noexcept; + /** Hash a 256-bit value as a jumbo block. For hash-table use, non-hash inputs must remain few and bounded. */ + SipHasher13UJ& WriteJumbo(const uint256& hash) noexcept; + /** Compute the 64-bit SipHash-1-3-UJ of the data written so far. The object remains untouched. */ + uint64_t Finalize() const noexcept; +}; + /** * Optimized SipHash-2-4 implementation for uint256. *