ppad-chacha 0.3.2 → 0.3.3
raw patch · 8 files changed
+438/−4 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Crypto.Cipher.ChaCha: FeedForwardAll :: FeedForward
+ Crypto.Cipher.ChaCha: FeedForwardKey :: FeedForward
+ Crypto.Cipher.ChaCha: _interleaved_into :: Rounds -> FeedForward -> Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> Int -> IO ()
+ Crypto.Cipher.ChaCha: _interleaved_pure_into :: Rounds -> FeedForward -> Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> Int -> IO ()
+ Crypto.Cipher.ChaCha: data FeedForward
+ Crypto.Cipher.ChaCha: instance GHC.Classes.Eq Crypto.Cipher.ChaCha.FeedForward
+ Crypto.Cipher.ChaCha: instance GHC.Show.Show Crypto.Cipher.ChaCha.FeedForward
Files
- CHANGELOG +5/−0
- bench/Main.hs +16/−0
- cbits/chacha20_native.c +199/−0
- lib/Crypto/Cipher/ChaCha.hs +96/−0
- lib/Crypto/Cipher/ChaCha20/Native.hs +19/−0
- ppad-chacha.cabal +1/−1
- test/Main.hs +60/−3
- test/Vectors.hs +42/−0
CHANGELOG view
@@ -1,5 +1,10 @@ # Changelog +- 0.3.3 (2026-11-14)+ * Adds four-block kernels producing interleaved keystream blocks,+ as used by ChaCha8Rand, and roughly twice as fast as the+ single-block kernels.+ - 0.3.2 (2026-11-14) * Adds Crypto.Cipher.ChaCha, with ChaCha keystreams of 8, 12, or 20 rounds.
bench/Main.hs view
@@ -12,7 +12,10 @@ import qualified Crypto.Cipher.ChaCha20 as ChaCha20 import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16+import qualified Data.ByteString.Internal as BI+import qualified Data.ByteString.Unsafe as BU import Data.Maybe (fromJust)+import Foreign.Ptr (castPtr) import GHC.Generics deriving instance Generic ChaCha20.Error@@ -23,6 +26,7 @@ main = defaultMain [ suite , keystream+ , interleaved ] plain :: BS.ByteString@@ -59,3 +63,15 @@ , bench "ChaCha20" $ nf (ChaCha.keystream ChaCha.ChaCha20 key 1 non) 1024 ] ++interleaved :: Benchmark+interleaved =+ bgroup "ppad-chacha (interleaved, 1024B)" [+ bench "ChaCha8" $ nfIO (inter ChaCha.ChaCha8 ChaCha.FeedForwardKey)+ , bench "ChaCha20" $ nfIO (inter ChaCha.ChaCha20 ChaCha.FeedForwardAll)+ ]+ where+ inter r ff = BI.create 1024 $ \dst ->+ BU.unsafeUseAsCString key $ \kp ->+ BU.unsafeUseAsCString non $ \np ->+ ChaCha._interleaved_into r ff (castPtr kp) 1 (castPtr np) dst 4
cbits/chacha20_native.c view
@@ -188,6 +188,68 @@ } } +/*+ * ChaCha NEON kernel using inter-block parallelism: four consecutive+ * blocks at once, with register x[i] holding word i of each block. A+ * quarter-round is then one set of element-wise operations on four+ * registers, with no lane rotation, and the four quarter-rounds of+ * each half-round are independent chains. The registers, stored in+ * order, give the blocks interleaved word by word.+ */+static inline void chacha20_core4v(uint32x4_t x[16], int drounds) {+ for (int i = 0; i < drounds; i++) {+ QUARTER(x[0], x[4], x[ 8], x[12]);+ QUARTER(x[1], x[5], x[ 9], x[13]);+ QUARTER(x[2], x[6], x[10], x[14]);+ QUARTER(x[3], x[7], x[11], x[15]);+ QUARTER(x[0], x[5], x[10], x[15]);+ QUARTER(x[1], x[6], x[11], x[12]);+ QUARTER(x[2], x[7], x[ 8], x[13]);+ QUARTER(x[3], x[4], x[ 9], x[14]);+ }+}++/*+ * Write 'ngroups' groups of four consecutive keystream blocks to+ * 'out', 256 bytes per group, using 'drounds' double rounds and+ * starting at block 'counter'. Within a group the blocks are+ * interleaved word by word: word i of block j is at byte offset+ * 16 * i + 4 * j. If 'key_ff_only' is nonzero, the final addition of+ * the input state is applied to the key words only, omitting the+ * public constant, counter, and nonce words, as in ChaCha8Rand.+ * 'out' must not overlap 'key' or 'nonce'.+ */+void chacha20_interleaved_native(const uint8_t key[32], uint32_t counter,+ const uint8_t nonce[12], uint8_t *out,+ size_t ngroups, int drounds,+ int key_ff_only) {+ uint32_t k[8], n[3];+ memcpy(k, key, 32);+ memcpy(n, nonce, 12);++ uint32x4_t s[16];+ for (int i = 0; i < 4; i++) s[i] = vdupq_n_u32(chacha_constants[i]);+ for (int i = 0; i < 8; i++) s[4 + i] = vdupq_n_u32(k[i]);+ for (int i = 0; i < 3; i++) s[13 + i] = vdupq_n_u32(n[i]);+ static const uint32_t lanes[4] = { 0, 1, 2, 3 };+ const uint32x4_t inc = vld1q_u32(lanes);++ for (size_t g = 0; g < ngroups; g++) {+ s[12] = vaddq_u32(vdupq_n_u32(counter), inc);+ uint32x4_t x[16];+ for (int i = 0; i < 16; i++) x[i] = s[i];+ chacha20_core4v(x, drounds);++ uint8_t *o = out + 256 * g;+ for (int i = 0; i < 16; i++) {+ int add = !key_ff_only || (i >= 4 && i < 12);+ uint32x4_t v = add ? vaddq_u32(x[i], s[i]) : x[i];+ vst1q_u8(o + 16 * i, vreinterpretq_u8_u32(v));+ }+ counter += 4;+ }+}+ int chacha20_native_available(void) { return 1; }@@ -577,6 +639,135 @@ } } +/* ChaCha core over eight consecutive blocks, one state word per+ * register, as in the four-block SSE2 kernel on wider registers. */+__attribute__((target("avx2")))+static inline void chacha20_core8v_avx2(__m256i x[16], int drounds) {+ for (int i = 0; i < drounds; i++) {+ QUARTER_AVX2(x[0], x[4], x[ 8], x[12]);+ QUARTER_AVX2(x[1], x[5], x[ 9], x[13]);+ QUARTER_AVX2(x[2], x[6], x[10], x[14]);+ QUARTER_AVX2(x[3], x[7], x[11], x[15]);+ QUARTER_AVX2(x[0], x[5], x[10], x[15]);+ QUARTER_AVX2(x[1], x[6], x[11], x[12]);+ QUARTER_AVX2(x[2], x[7], x[ 8], x[13]);+ QUARTER_AVX2(x[3], x[4], x[ 9], x[14]);+ }+}++/*+ * Write as many pairs of interleaved four-block groups as 'ngroups'+ * allows, eight blocks per pass: the low 128-bit lane of each+ * register holds the first group's word, the high lane the second's.+ * Returns the number of groups written.+ */+__attribute__((target("avx2")))+static size_t chacha20_interleaved_avx2(const uint8_t key[32],+ uint32_t counter,+ const uint8_t nonce[12],+ uint8_t *out, size_t ngroups,+ int drounds, int key_ff_only) {+ uint32_t k[8], n[3];+ memcpy(k, key, 32);+ memcpy(n, nonce, 12);++ __m256i s[16];+ for (int i = 0; i < 4; i++)+ s[i] = _mm256_set1_epi32((int)chacha_constants[i]);+ for (int i = 0; i < 8; i++) s[4 + i] = _mm256_set1_epi32((int)k[i]);+ for (int i = 0; i < 3; i++) s[13 + i] = _mm256_set1_epi32((int)n[i]);+ const __m256i inc = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);++ size_t g = 0;+ for (; g + 2 <= ngroups; g += 2) {+ s[12] = _mm256_add_epi32(_mm256_set1_epi32((int)counter), inc);+ __m256i x[16];+ for (int i = 0; i < 16; i++) x[i] = s[i];+ chacha20_core8v_avx2(x, drounds);++ uint8_t *o = out + 256 * g;+ for (int i = 0; i < 16; i++) {+ int add = !key_ff_only || (i >= 4 && i < 12);+ __m256i v = add ? _mm256_add_epi32(x[i], s[i]) : x[i];+ _mm_storeu_si128((__m128i *)(o + 16 * i),+ _mm256_castsi256_si128(v));+ _mm_storeu_si128((__m128i *)(o + 256 + 16 * i),+ _mm256_extracti128_si256(v, 1));+ }+ counter += 8;+ }+ return g;+}++/*+ * ChaCha SSE2 kernel using inter-block parallelism, as in the NEON+ * kernel of the same name: register x[i] holds word i of each of+ * four consecutive blocks.+ */+static inline void chacha20_core4v(__m128i x[16], int drounds) {+ for (int i = 0; i < drounds; i++) {+ QUARTER(x[0], x[4], x[ 8], x[12]);+ QUARTER(x[1], x[5], x[ 9], x[13]);+ QUARTER(x[2], x[6], x[10], x[14]);+ QUARTER(x[3], x[7], x[11], x[15]);+ QUARTER(x[0], x[5], x[10], x[15]);+ QUARTER(x[1], x[6], x[11], x[12]);+ QUARTER(x[2], x[7], x[ 8], x[13]);+ QUARTER(x[3], x[4], x[ 9], x[14]);+ }+}++/*+ * Write 'ngroups' groups of four consecutive keystream blocks to+ * 'out', 256 bytes per group, using 'drounds' double rounds and+ * starting at block 'counter'. Within a group the blocks are+ * interleaved word by word: word i of block j is at byte offset+ * 16 * i + 4 * j. If 'key_ff_only' is nonzero, the final addition of+ * the input state is applied to the key words only, omitting the+ * public constant, counter, and nonce words, as in ChaCha8Rand.+ * 'out' must not overlap 'key' or 'nonce'.+ */+void chacha20_interleaved_native(const uint8_t key[32], uint32_t counter,+ const uint8_t nonce[12], uint8_t *out,+ size_t ngroups, int drounds,+ int key_ff_only) {+ uint32_t k[8], n[3];+ memcpy(k, key, 32);+ memcpy(n, nonce, 12);++ /* pairs of groups via AVX2 where available, the rest via SSE2 */+ if (ngroups >= 2 && have_avx2()) {+ size_t done = chacha20_interleaved_avx2(key, counter, nonce, out,+ ngroups, drounds,+ key_ff_only);+ counter += (uint32_t)(4 * done);+ out += 256 * done;+ ngroups -= done;+ }++ __m128i s[16];+ for (int i = 0; i < 4; i++)+ s[i] = _mm_set1_epi32((int)chacha_constants[i]);+ for (int i = 0; i < 8; i++) s[4 + i] = _mm_set1_epi32((int)k[i]);+ for (int i = 0; i < 3; i++) s[13 + i] = _mm_set1_epi32((int)n[i]);+ const __m128i inc = _mm_setr_epi32(0, 1, 2, 3);++ for (size_t g = 0; g < ngroups; g++) {+ s[12] = _mm_add_epi32(_mm_set1_epi32((int)counter), inc);+ __m128i x[16];+ for (int i = 0; i < 16; i++) x[i] = s[i];+ chacha20_core4v(x, drounds);++ uint8_t *o = out + 256 * g;+ for (int i = 0; i < 16; i++) {+ int add = !key_ff_only || (i >= 4 && i < 12);+ __m128i v = add ? _mm_add_epi32(x[i], s[i]) : x[i];+ _mm_storeu_si128((__m128i *)(o + 16 * i), v);+ }+ counter += 4;+ }+}+ int chacha20_native_available(void) { return 1; }@@ -598,6 +789,14 @@ size_t inlen, int drounds) { (void)key; (void)counter; (void)nonce; (void)in; (void)out; (void)inlen; (void)drounds;+}++void chacha20_interleaved_native(const uint8_t *key, uint32_t counter,+ const uint8_t *nonce, uint8_t *out,+ size_t ngroups, int drounds,+ int key_ff_only) {+ (void)key; (void)counter; (void)nonce; (void)out;+ (void)ngroups; (void)drounds; (void)key_ff_only; } int chacha20_native_available(void) {
lib/Crypto/Cipher/ChaCha.hs view
@@ -25,20 +25,26 @@ , Error(..) -- internal+ , FeedForward(..) , _keystream_into+ , _interleaved_into+ , _interleaved_pure_into ) where import Control.Monad.ST (runST)+import Data.Bits ((.|.), (.<<.), (.>>.)) import Crypto.Cipher.ChaCha20 (Error(..)) import qualified Crypto.Cipher.ChaCha20 as ChaCha20 import qualified Crypto.Cipher.ChaCha20.Native as Native import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BI import qualified Data.ByteString.Unsafe as BU+import Data.Foldable (for_) import Data.Word (Word8, Word32, Word64) import Foreign.ForeignPtr (withForeignPtr) import Foreign.Marshal.Utils (copyBytes) import Foreign.Ptr (Ptr, castPtr, plusPtr)+import Foreign.Storable (pokeByteOff) -- utils ---------------------------------------------------------------------- @@ -130,3 +136,93 @@ (ChaCha20._parse_nonce non) (BS.replicate len 0) BU.unsafeUseAsCString out $ \src -> copyBytes dst (castPtr src) len++-- interleaved keystream ------------------------------------------------------++-- | Which words of the input state are added to a block's output.+data FeedForward =+ FeedForwardAll -- ^ all words, as standard+ | FeedForwardKey -- ^ the key words only, omitting the public+ -- constant, counter, and nonce words, as in+ -- ChaCha8Rand+ deriving (Eq, Show)++-- | Write groups of four consecutive keystream blocks to the+-- destination pointer, interleaved word by word: within each+-- 256-byte group, word i of block j is at byte offset 16 * i + 4 * j.+-- This is the layout that four-way SIMD produces naturally.+--+-- Unchecked: the key and nonce pointers must address 32 and 12+-- bytes respectively, the destination must have room for 256 bytes+-- per group and must not overlap the key or nonce, and the counter+-- must leave room for four blocks per group.+_interleaved_into+ :: Rounds -- ^ number of rounds+ -> FeedForward -- ^ final addition of the input state+ -> Ptr Word8 -- ^ 256-bit key+ -> Word32 -- ^ 32-bit counter+ -> Ptr Word8 -- ^ 96-bit nonce+ -> Ptr Word8 -- ^ destination+ -> Int -- ^ number of four-block groups+ -> IO ()+_interleaved_into r ff kp counter np dst groups+ | Native.chacha20_native_available =+ Native.interleaved_into (drounds r) (ff == FeedForwardKey) kp+ counter np dst groups+ | otherwise = _interleaved_pure_into r ff kp counter np dst groups++-- | A pure Haskell '_interleaved_into', with the same preconditions.+_interleaved_pure_into+ :: Rounds+ -> FeedForward+ -> Ptr Word8+ -> Word32+ -> Ptr Word8+ -> Ptr Word8+ -> Int+ -> IO ()+_interleaved_pure_into r ff kp counter np dst groups = do+ key <- BS.packCStringLen (castPtr kp, 32)+ non <- BS.packCStringLen (castPtr np, 12)+ let !ks = runST $+ ChaCha20._cipher_rounds (drounds r)+ (ChaCha20._parse_key key) counter (ChaCha20._parse_nonce non)+ (BS.replicate (256 * groups) 0)+ -- the input word that the final addition omits+ omit i blk = case ff of+ FeedForwardAll -> 0+ FeedForwardKey+ | i < 4 -> constant i+ | i == 12 -> counter + fi blk+ | i > 12 -> le32_at non (4 * (i - 13))+ | otherwise -> 0+ for_ [0 .. groups - 1] $ \g ->+ for_ [0 .. 15] $ \i ->+ for_ [0 .. 3] $ \j -> do+ let !blk = 4 * g + j+ !v = le32_at ks (64 * blk + 4 * i) - omit i blk+ poke_le32 (dst `plusPtr` (256 * g + 16 * i + 4 * j)) v+ where+ constant :: Int -> Word32+ constant i = case i of+ 0 -> 0x61707865+ 1 -> 0x3320646e+ 2 -> 0x79622d32+ _ -> 0x6b206574++-- little-endian word at the given (in-bounds) offset+le32_at :: BS.ByteString -> Int -> Word32+le32_at bs o =+ fi (BU.unsafeIndex bs o)+ .|. (fi (BU.unsafeIndex bs (o + 1)) .<<. 8)+ .|. (fi (BU.unsafeIndex bs (o + 2)) .<<. 16)+ .|. (fi (BU.unsafeIndex bs (o + 3)) .<<. 24)+{-# INLINE le32_at #-}++poke_le32 :: Ptr Word8 -> Word32 -> IO ()+poke_le32 p w = do+ pokeByteOff p 0 (fi w :: Word8)+ pokeByteOff p 1 (fi (w .>>. 8) :: Word8)+ pokeByteOff p 2 (fi (w .>>. 16) :: Word8)+ pokeByteOff p 3 (fi (w .>>. 24) :: Word8)+{-# INLINE poke_le32 #-}
lib/Crypto/Cipher/ChaCha20/Native.hs view
@@ -14,6 +14,7 @@ , block , cipher , keystream_into+ , interleaved_into ) where import qualified Data.ByteString as BS@@ -36,6 +37,11 @@ :: Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> CSize -> CInt -> IO () +foreign import ccall unsafe "chacha20_interleaved_native"+ c_chacha20_interleaved+ :: Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> CSize -> CInt+ -> CInt -> IO ()+ foreign import ccall unsafe "chacha20_native_available" c_chacha20_native_available :: IO CInt @@ -97,3 +103,16 @@ keystream_into drounds kp counter np dst len = do fillBytes dst 0 len c_chacha20_cipher kp counter np dst dst (fi len) (fi drounds)++-- | Write 'groups' groups of four keystream blocks, interleaved word+-- by word, using the given number of double rounds, for the 32-byte+-- key and 12-byte nonce at the given pointers, to the destination,+-- which must not overlap them. If the flag is set, the final+-- addition of the input state covers the key words only. The+-- counter must leave room for four blocks per group.+interleaved_into+ :: Int -> Bool -> Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> Int+ -> IO ()+interleaved_into drounds key_only kp counter np dst groups =+ c_chacha20_interleaved kp counter np dst (fi groups) (fi drounds)+ (if key_only then 1 else 0)
ppad-chacha.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-chacha-version: 0.3.2+version: 0.3.3 synopsis: A fast ChaCha20 stream cipher license: MIT license-file: LICENSE
test/Main.hs view
@@ -17,11 +17,11 @@ import Data.Foldable (for_) import Data.Maybe (fromJust) import qualified Data.Primitive.PrimArray as PA-import Data.Word (Word32)-import Foreign.Ptr (castPtr)+import Data.Word (Word8, Word32)+import Foreign.Ptr (Ptr, castPtr) import Test.Tasty import qualified Test.Tasty.HUnit as H-import Vectors (V(..), vectors)+import Vectors (V(..), vectors, chacha8rand_seed, chacha8rand_iteration) main :: IO () main = defaultMain $ testGroup "ppad-chacha" [@@ -42,6 +42,11 @@ , keystream_dispatch , keystream_into ]+ , testGroup "interleaved" [+ interleaved_chacha8rand+ , interleaved_layout+ , interleaved_dispatch+ ] ] quarter :: TestTree@@ -287,3 +292,55 @@ BU.unsafeUseAsCString block_non $ \np -> Reduced._keystream_into r (castPtr kp) 7 (castPtr np) dst 1000 H.assertEqual (show r) ref out++-- interleaved++type Interleave =+ Reduced.Rounds -> Reduced.FeedForward -> Ptr Word8 -> Word32+ -> Ptr Word8 -> Ptr Word8 -> Int -> IO ()++interleave+ :: Interleave -> Reduced.Rounds -> Reduced.FeedForward -> BS.ByteString+ -> Word32 -> BS.ByteString -> Int -> IO BS.ByteString+interleave f r ff key con non groups =+ BI.create (256 * groups) $ \dst ->+ BU.unsafeUseAsCString key $ \kp ->+ BU.unsafeUseAsCString non $ \np ->+ f r ff (castPtr kp) con (castPtr np) dst groups++-- ChaCha8Rand's iteration is sixteen ChaCha8 blocks, interleaved, with+-- the key-only final addition; its first 992 bytes are output+interleaved_chacha8rand :: TestTree+interleaved_chacha8rand = H.testCase "ChaCha8Rand iteration" $ do+ out <- interleave Reduced._interleaved_into Reduced.ChaCha8+ Reduced.FeedForwardKey chacha8rand_seed 0 (BS.replicate 12 0) 4+ H.assertEqual mempty (hex chacha8rand_iteration) (BS.take 992 out)++-- with the full final addition, the output is the standard keystream+-- with each group of four blocks transposed+interleaved_layout :: TestTree+interleaved_layout = H.testCase "layout (vs keystream)" $+ for_ [Reduced.ChaCha8, Reduced.ChaCha12, Reduced.ChaCha20] $ \r -> do+ let Right ks = Reduced.keystream r block_key 7 block_non (256 * 3)+ word b i = BS.take 4 (BS.drop (64 * b + 4 * i) ks)+ ex = BS.concat [ word (4 * g + j) i+ | g <- [0 .. 2], i <- [0 .. 15], j <- [0 .. 3] ]+ out <- interleave Reduced._interleaved_into r Reduced.FeedForwardAll+ block_key 7 block_non 3+ H.assertEqual (show r) ex out++-- the dispatching '_interleaved_into' agrees with the pure Haskell+-- implementation; the second counter puts the final block of six+-- groups at the maximum counter value+interleaved_dispatch :: TestTree+interleaved_dispatch = H.testCase "dispatch vs pure" $+ for_ [Reduced.ChaCha8, Reduced.ChaCha12, Reduced.ChaCha20] $ \r ->+ for_ [Reduced.FeedForwardAll, Reduced.FeedForwardKey] $ \ff ->+ for_ [block_key, BS.replicate 32 0xff] $ \key ->+ for_ [1, maxBound - 23] $ \con ->+ for_ [1 .. 6] $ \groups -> do+ out <- interleave Reduced._interleaved_into r ff key con block_non+ groups+ ref <- interleave Reduced._interleaved_pure_into r ff key con+ block_non groups+ H.assertEqual (show (r, ff, con, groups)) ref out
test/Vectors.hs view
@@ -8,6 +8,8 @@ module Vectors ( V(..) , vectors+ , chacha8rand_seed+ , chacha8rand_iteration ) where import Crypto.Cipher.ChaCha (Rounds(..))@@ -239,4 +241,44 @@ , "e5fbc34e60a1d9a9db17345b0a402736853bf910b060bdf1f897b6290f01d138" , "ae2c4c90225ba9ea14d518f55929dea098ca7a6ccfe61227053c84e49a4a3332" ])+ ]++-- the first iteration of ChaCha8Rand output (992 bytes) for this seed,+-- from the sample output in etc/chacha8rand.md+chacha8rand_seed :: BS.ByteString+chacha8rand_seed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"++chacha8rand_iteration :: BS.ByteString+chacha8rand_iteration = mconcat [+ "a516463d06b673b73cbc6aa622af60117c288d41d999258cd65cdc7e037ee07e"+ , "ad161c2de09eaacf79eaeb8fef0e090e3e5b8b1271d2823c4fa35212c1dd5a9c"+ , "a6ee6c7d61bb79df6a739d1f5953d5361fe01e184ed1f0ee3684e50a76fc9b08"+ , "68d22acc592be5d9ba8a1b4b44b42feb6166c492a6c8954f2ce6ca173232c6c3"+ , "7e2e4e7f36b4eb91c69beca0c6f24c7820beea345cec345ca8da7055518f0a4f"+ , "f26b3d11b4dc35fcbc5455644ca40d5be1d921dba33d966df300e55031fcaeee"+ , "a55037da3d92372db0c84b6d627a0d3849eed7e3ed68afee7c713b88956635f4"+ , "a495243921906a841ba630965410858e93e4ba5d5402dc18433d5aa6f09f8f0f"+ , "80f00f19f765f0cc30336739aad176fd3364ba6c9332d29517bd0c07d156746c"+ , "62658cffdacf2a466afc346d86abaf5b88290a03782f860c3d16c307e4839ad3"+ , "bf2ef2457b2b0ac0a9b16624c6074356d472c0b024047e25fe286c49995eb56f"+ , "e0d45c8fa87398ae73370dc62a3657468e3ef2cd1ef9831cc08753c19207dc6f"+ , "5c2bfd0da3d2da36b7bd95052993b5a4c502cce43489e14da7e315f004d6c0cd"+ , "2103d89af6dba0fb87de39d1a3bee8605687f41e854d8ad14af315227c446663"+ , "ee07d0d3972e6805b24ad5c678890e4cc2ed12676a9f1ecfd3cf804c41391406"+ , "ad0e5c74e2b6a8d1e810c4458d91a731ec6e2190ad61ccab1aa732202dd94040"+ , "68cd40fb6ff6d23c7a859572c051d0dc7e52abd9bc5cb5eaacda1b78ce1d4718"+ , "5272dc44d18cf0f7d1407f3db1e004585bd3b2e446a4b15c068a13d228a7d4e6"+ , "d8da60ca403e2205686aac0632ec612db8174c87562369ab1cde7676415409c3"+ , "2456223237ce1a4f388398130851a9fb112e75520f207f99fa2162e8afaa1611"+ , "1935a1b25c3bce07148345bc72bc562978eb406192b788414deaadd4bf6dca56"+ , "e50c344923c2e37f8a5f67379c8fc03521d55eeffbc7e11175bcc14e46c8ad98"+ , "f803123dc7b263d1f3f3a243e01e768cb7d7eccc6a9db92470032f11aa313e79"+ , "395128192adc878e256e09f704ae47420fe26f9222314f51e9a7d245f0b36fdc"+ , "ba8ed1cdce30cb15740290f6ecfdc7cb21a08bdc96c6b53fe674d2c8174466d1"+ , "787245ea45e4f70557b69d1bcabb20f999cb22dab450190c92e209aff1ba75f8"+ , "38f85042b8d7d3be6041d70f08e898f103a77e9b1da5edc9f6f89b4355ef09f7"+ , "fc16f1ebfe740cd246756d14eb6856308797d810ecf39a82bc1d557b69f9b815"+ , "c9b8648e6c3c82fcbc403b18e8855534681358d671414b67f7e970d61cd83412"+ , "195ea5d81052500edca0ecee698d25e87ef6bae852c4d4059955a41601e3dbe8"+ , "006f17b1e18cf01c49cb1eb8a4d0f7cc0e432c6b13ea3f3071c8069c136c1d86" ]