ppad-sha512 0.2.2 → 0.2.3
raw patch · 3 files changed
+89/−19 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG +7/−0
- lib/Crypto/Hash/SHA512/Internal.hs +81/−18
- ppad-sha512.cabal +1/−1
CHANGELOG view
@@ -1,5 +1,12 @@ # Changelog +- 0.2.3 (2026-07-06)+ * The constant-time 'Eq' on 'MAC' now folds over the two buffers+ directly, rather than materialising an intermediate ByteString+ holding their XOR on the heap. Semantics are unchanged.++ * Improves the performance of constant-time MAC comparison.+ - 0.2.2 (2026-02-01) * The library has been refactored substantially to achieve greater control over heap allocation, particularly around HMAC calculation.
lib/Crypto/Hash/SHA512/Internal.hs view
@@ -83,8 +83,44 @@ -- -- Runs in variable-time only for invalid inputs. (MAC a@(BI.PS _ _ la)) == (MAC b@(BI.PS _ _ lb))- | la /= lb = False- | otherwise = BS.foldl' (B..|.) 0 (BS.packZipWith B.xor a b) == 0+ | la /= lb = False+ | la == 64 =+ -- fully-unrolled, fixed OR-tree over eight 64-bit lanes.+ -- A standard 64-byte MAC folds through no loop-carried+ -- accumulator, so there is no per-byte accumulator whose+ -- nonzero span tracks the position of a differing byte:+ -- the comparison time is independent of where (or whether)+ -- the tags differ.+ let !d0 = Exts.xor64# (word64le a 00) (word64le b 00)+ !d1 = Exts.xor64# (word64le a 08) (word64le b 08)+ !d2 = Exts.xor64# (word64le a 16) (word64le b 16)+ !d3 = Exts.xor64# (word64le a 24) (word64le b 24)+ !d4 = Exts.xor64# (word64le a 32) (word64le b 32)+ !d5 = Exts.xor64# (word64le a 40) (word64le b 40)+ !d6 = Exts.xor64# (word64le a 48) (word64le b 48)+ !d7 = Exts.xor64# (word64le a 56) (word64le b 56)+ !dl = (d0 `Exts.or64#` d1) `Exts.or64#`+ (d2 `Exts.or64#` d3)+ !dh = (d4 `Exts.or64#` d5) `Exts.or64#`+ (d6 `Exts.or64#` d7)+ !d = dl `Exts.or64#` dh+ in Exts.isTrue# (Exts.eqWord64# d (Exts.wordToWord64# 0##))+ | otherwise = go 0 0+ where+ -- byte-serial fallback for nonstandard MAC lengths. 'hmac'+ -- always yields 64 bytes, so this path is unreachable through+ -- it; it exists only to keep the instance total for MACs built+ -- directly via the exported constructor. The fused fold ORs the+ -- bytewise XORs into an accumulator directly, rather than via+ -- packZipWith, so no intermediate ByteString holding the+ -- (secret-derived) difference bytes is ever materialised.+ go :: Word8 -> Int -> Bool+ go !acc !i+ | i == la = acc == 0+ | otherwise =+ let !x = BU.unsafeIndex a i+ !y = BU.unsafeIndex b i+ in go (acc B..|. B.xor x y) (i + 1) -- | SHA512 block. newtype Block = Block@@ -181,6 +217,41 @@ s4 `Exts.or#` s5 `Exts.or#` s6 `Exts.or#` w7) {-# INLINE word64be #-} +-- | Assemble the 64-bit word at the given byte offset, little-endian.+--+-- Byte order is immaterial to an equality test as long as both+-- operands are assembled the same way; this is used only by the+-- constant-time 'MAC' comparison. The length is not checked.+word64le :: BS.ByteString -> Int -> Exts.Word64#+word64le bs m =+ let !(GHC.Word.W8# r0) = BU.unsafeIndex bs m+ !(GHC.Word.W8# r1) = BU.unsafeIndex bs (m + 1)+ !(GHC.Word.W8# r2) = BU.unsafeIndex bs (m + 2)+ !(GHC.Word.W8# r3) = BU.unsafeIndex bs (m + 3)+ !(GHC.Word.W8# r4) = BU.unsafeIndex bs (m + 4)+ !(GHC.Word.W8# r5) = BU.unsafeIndex bs (m + 5)+ !(GHC.Word.W8# r6) = BU.unsafeIndex bs (m + 6)+ !(GHC.Word.W8# r7) = BU.unsafeIndex bs (m + 7)+ !w0 = Exts.word8ToWord# r0+ !w1 = Exts.word8ToWord# r1+ !w2 = Exts.word8ToWord# r2+ !w3 = Exts.word8ToWord# r3+ !w4 = Exts.word8ToWord# r4+ !w5 = Exts.word8ToWord# r5+ !w6 = Exts.word8ToWord# r6+ !w7 = Exts.word8ToWord# r7+ !s1 = Exts.uncheckedShiftL# w1 8#+ !s2 = Exts.uncheckedShiftL# w2 16#+ !s3 = Exts.uncheckedShiftL# w3 24#+ !s4 = Exts.uncheckedShiftL# w4 32#+ !s5 = Exts.uncheckedShiftL# w5 40#+ !s6 = Exts.uncheckedShiftL# w6 48#+ !s7 = Exts.uncheckedShiftL# w7 56#+ in Exts.wordToWord64#+ (w0 `Exts.or#` s1 `Exts.or#` s2 `Exts.or#` s3 `Exts.or#`+ s4 `Exts.or#` s5 `Exts.or#` s6 `Exts.or#` s7)+{-# INLINE word64le #-}+ -- parsing (final input) ------------------------------------------------------ -- | Parse the final chunk of an input message, assuming it is less than@@ -812,22 +883,14 @@ !(GHC.Word.W8# b4) = BU.unsafeIndex dat 4 !(GHC.Word.W8# b5) = BU.unsafeIndex dat 5 !(GHC.Word.W8# b6) = BU.unsafeIndex dat 6- !w08 =- Exts.uncheckedShiftL# (Exts.word8ToWord# sep) 56#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b0) 48#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b1) 40#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b2) 32#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b3) 24#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b4) 16#- `Exts.or#`- Exts.uncheckedShiftL# (Exts.word8ToWord# b5) 8#- `Exts.or#`- Exts.word8ToWord# b6+ !w08 = Exts.uncheckedShiftL# (Exts.word8ToWord# sep) 56#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b0) 48#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b1) 40#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b2) 32#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b3) 24#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b4) 16#+ `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b5) 08#+ `Exts.or#` Exts.word8ToWord# b6 in B v0 v1 v2 v3 v4 v5 v6 v7 (Exts.wordToWord64# w08) (word64be dat 07) (word64be dat 15) (word64be dat 23)
ppad-sha512.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-sha512-version: 0.2.2+version: 0.2.3 synopsis: The SHA-512 and HMAC-SHA512 algorithms license: MIT license-file: LICENSE