ppad-random (empty) → 0.1.0
raw patch · 13 files changed
+2016/−0 lines, 13 filesdep +basedep +bytestringdep +criterion
Dependencies added: base, bytestring, criterion, crypton, entropy, mwc-random, pcg-random, ppad-base16, ppad-chacha, ppad-random, primitive, random, splitmix, tasty, tasty-hunit, weigh
Files
- CHANGELOG +5/−0
- LICENSE +20/−0
- bench/Main.hs +126/−0
- bench/Weight.hs +58/−0
- internal/Crypto/Range.hs +106/−0
- lib/Crypto/CSPRNG.hs +324/−0
- lib/Crypto/Entropy.hs +30/−0
- lib/Crypto/PRNG.hs +228/−0
- lib/Crypto/PRNG/Pure.hs +179/−0
- lib/Crypto/Xoshiro.hs +135/−0
- ppad-random.cabal +106/−0
- test/Main.hs +457/−0
- test/Vectors.hs +242/−0
+ CHANGELOG view
@@ -0,0 +1,5 @@+# Changelog++- 0.1.0 (2026-11-14)+ * Initial release, supporting the xoshiro256++ PRNG, both mutable+ and pure, and the ChaCha8Rand CSPRNG.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2026 Jared Tobin++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ bench/Main.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE BangPatterns #-}++module Main where++import Criterion.Main+import qualified Crypto.CSPRNG as CSPRNG+import qualified Crypto.PRNG as PRNG+import qualified Crypto.PRNG.Pure as Pure+import qualified Crypto.Random as Crypton+import qualified Data.ByteString as BS+import Data.Word (Word64)+import GHC.Exts (RealWorld)+import qualified System.Entropy as Entropy+import qualified System.Random.MWC as MWC+import qualified System.Random.PCG as PCG+import qualified System.Random.SplitMix as SM+import qualified System.Random.Stateful as R++main :: IO ()+main = do+ prng <- PRNG.new 42+ mc <- CSPRNG.new (BS.replicate 32 0)+ mwc <- MWC.create+ pcg <- PCG.create+ std <- R.newIOGenM (R.mkStdGen 42)+ drg <- Crypton.drgNew+ let !sm = SM.mkSMGen 42+ !pg = Pure.new 42+ case mc of+ Nothing -> error "ppad-random (bench): invalid seed"+ Just csprng -> defaultMain [+ bgroup "word64 (x1000)" [+ bench "ppad-random (PRNG)" $+ whnfIO (sum_n (PRNG.word64 prng) 1000)+ , bench "ppad-random (PRNG.Pure)" $+ whnf (sum_draws Pure.word64 pg) 1000+ , bench "ppad-random (CSPRNG)" $+ whnfIO (sum_n (CSPRNG.word64 csprng) 1000)+ , bench "mwc-random" $+ whnfIO (sum_n (MWC.uniform mwc :: IO Word64) 1000)+ , bench "pcg-random" $+ whnfIO (sum_n (PCG.uniform pcg :: IO Word64) 1000)+ , bench "random (IOGenM StdGen)" $+ whnfIO (sum_n (R.uniformM std :: IO Word64) 1000)+ , bench "splitmix (pure)" $+ whnf (sum_pure SM.nextWord64 sm) 1000+ ]+ , bgroup "double (x1000)" [+ bench "ppad-random (PRNG)" $+ whnfIO (sum_n (PRNG.double prng) 1000)+ , bench "ppad-random (PRNG.Pure)" $+ whnf (sum_draws Pure.double pg) 1000+ , bench "mwc-random" $+ whnfIO (sum_n (MWC.uniform mwc :: IO Double) 1000)+ , bench "pcg-random" $+ whnfIO (sum_n (PCG.uniform pcg :: IO Double) 1000)+ , bench "random (IOGenM StdGen)" $+ whnfIO (sum_n (R.uniformDouble01M std) 1000)+ , bench "splitmix (pure)" $+ whnf (sum_pure SM.nextDouble sm) 1000+ ]+ , bgroup "int_range 1 6 (x1000)" [+ bench "ppad-random (PRNG)" $+ whnfIO (sum_n (PRNG.int_range 1 6 prng) 1000)+ , bench "ppad-random (PRNG.Pure)" $+ whnf (sum_draws (Pure.int_range 1 6) pg) 1000+ , bench "ppad-random (CSPRNG)" $+ whnfIO (sum_n (CSPRNG.int_range 1 6 csprng) 1000)+ , bench "mwc-random" $+ whnfIO (sum_n (MWC.uniformR (1, 6) mwc :: IO Int) 1000)+ , bench "pcg-random" $+ whnfIO (sum_n (PCG.uniformR (1, 6) pcg :: IO Int) 1000)+ , bench "random (IOGenM StdGen)" $+ whnfIO (sum_n (R.uniformRM (1, 6) std :: IO Int) 1000)+ ]+ , bgroup "bytes (32B)" [+ bench "ppad-random (CSPRNG)" $ nfIO (CSPRNG.bytes 32 csprng)+ , bench "crypton (ChaChaDRG)" $ nf (crypton_bytes drg) 32+ , bench "entropy (getEntropy)" $ nfIO (Entropy.getEntropy 32)+ ]+ , bgroup "bytes (1024B)" [+ bench "ppad-random (CSPRNG)" $ nfIO (CSPRNG.bytes 1024 csprng)+ , bench "crypton (ChaChaDRG)" $ nf (crypton_bytes drg) 1024+ , bench "entropy (getEntropy)" $ nfIO (Entropy.getEntropy 1024)+ ]+ , bgroup "CSPRNG" [+ bench "split" $ whnfIO (split_csprng csprng)+ ]+ ]++-- sum the results of running an action n times+sum_n :: Num a => IO a -> Int -> IO a+sum_n gen = loop 0 where+ loop !acc n+ | n <= 0 = pure acc+ | otherwise = do+ !x <- gen+ loop (acc + x) (n - 1)+{-# INLINE sum_n #-}++-- sum the results of stepping a pure generator n times+sum_pure :: Num a => (g -> (a, g)) -> g -> Int -> a+sum_pure step = loop 0 where+ loop !acc !g n+ | n <= 0 = acc+ | otherwise =+ let !(x, ng) = step g+ in loop (acc + x) ng (n - 1)+{-# INLINE sum_pure #-}++crypton_bytes :: Crypton.ChaChaDRG -> Int -> BS.ByteString+crypton_bytes drg n = fst (Crypton.randomBytesGenerate n drg)++split_csprng :: CSPRNG.Gen RealWorld -> IO ()+split_csprng g = do+ !_ <- CSPRNG.split g+ pure ()++-- sum the results of n draws from a pure generator+sum_draws :: Num a => (Pure.Gen -> Pure.Draw a) -> Pure.Gen -> Int -> a+sum_draws f = loop 0 where+ loop !acc !g n+ | n <= 0 = acc+ | otherwise = case f g of+ Pure.Draw x ng -> loop (acc + x) ng (n - 1)+{-# INLINE sum_draws #-}
+ bench/Weight.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE BangPatterns #-}++module Main where++import qualified Crypto.CSPRNG as CSPRNG+import qualified Crypto.PRNG as PRNG+import qualified Crypto.PRNG.Pure as Pure+import qualified Data.ByteString as BS+import Data.Word (Word64)+import Weigh++-- note that 'weigh' doesn't work properly in a repl+main :: IO ()+main = do+ prng <- PRNG.new 42+ let !pg = Pure.new 42+ mc <- CSPRNG.new (BS.replicate 32 0)+ case mc of+ Nothing -> error "ppad-random (weigh): invalid seed"+ Just csprng -> mainWith $ do+ wgroup "PRNG" $ do+ io "word64 (x1000)" (sum_words (PRNG.word64 prng)) 1000+ io "double (x1000)" (sum_doubles (PRNG.double prng)) 1000+ wgroup "PRNG.Pure" $ do+ func "word64 (x1000)" (sum_draws Pure.word64 pg) 1000+ func "double (x1000)" (sum_draws Pure.double pg) 1000+ func "int_range 1 6 (x1000)" (sum_draws (Pure.int_range 1 6) pg) 1000+ wgroup "CSPRNG" $ do+ io "word64 (x1000)" (sum_words (CSPRNG.word64 csprng)) 1000+ io "bytes (32B)" (\n -> CSPRNG.bytes n csprng) 32+ io "bytes (1024B)" (\n -> CSPRNG.bytes n csprng) 1024++sum_words :: IO Word64 -> Int -> IO Word64+sum_words gen = loop 0 where+ loop !acc n+ | n <= 0 = pure acc+ | otherwise = do+ !w <- gen+ loop (acc + w) (n - 1)+{-# INLINE sum_words #-}++sum_doubles :: IO Double -> Int -> IO Double+sum_doubles gen = loop 0 where+ loop !acc n+ | n <= 0 = pure acc+ | otherwise = do+ !d <- gen+ loop (acc + d) (n - 1)+{-# INLINE sum_doubles #-}++-- sum the results of n draws from a pure generator+sum_draws :: Num a => (Pure.Gen -> Pure.Draw a) -> Pure.Gen -> Int -> a+sum_draws f = loop 0 where+ loop !acc !g n+ | n <= 0 = acc+ | otherwise = case f g of+ Pure.Draw x ng -> loop (acc + x) ng (n - 1)+{-# INLINE sum_draws #-}
+ internal/Crypto/Range.hs view
@@ -0,0 +1,106 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE UnboxedTuples #-}++-- |+-- Module: Crypto.Range+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- Unbiased integers in a range, via Lemire's nearly-divisionless+-- method ("Fast Random Integer Generation in an Interval", 2019).++module Crypto.Range (+ upto+ , upto_pure+ , W2(..)+ , mul_wide+ , _mul_wide_limbs+ ) where++import Data.Bits ((.&.), (.>>.))+import qualified Data.Bits as B+import Data.Word (Word64)+import GHC.Exts (Word(..), timesWord2#)++-- | A 128-bit product, as its high and low words.+data W2 = W2 {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64+ deriving (Eq, Show)++-- | The full 128-bit product of two words.+mul_wide :: Word64 -> Word64 -> W2+mul_wide a b+ | B.finiteBitSize (0 :: Word) == 64 =+ let !(W# x) = fromIntegral a+ !(W# y) = fromIntegral b+ !(# h, l #) = timesWord2# x y+ in W2 (fromIntegral (W# h)) (fromIntegral (W# l))+ | otherwise = _mul_wide_limbs a b+{-# INLINE mul_wide #-}++-- | The full 128-bit product of two words, via 32-bit limbs, for+-- platforms with 32-bit machine words.+_mul_wide_limbs :: Word64 -> Word64 -> W2+_mul_wide_limbs a b =+ let !a0 = a .&. 0xffffffff+ !a1 = a .>>. 32+ !b0 = b .&. 0xffffffff+ !b1 = b .>>. 32+ !p00 = a0 * b0+ !p01 = a0 * b1+ !p10 = a1 * b0+ !p11 = a1 * b1+ !mid = (p00 .>>. 32) + (p01 .&. 0xffffffff) + (p10 .&. 0xffffffff)+ !hi = p11 + (p01 .>>. 32) + (p10 .>>. 32) + (mid .>>. 32)+ in W2 hi (a * b)+{-# INLINE _mul_wide_limbs #-}++-- | A uniformly-distributed word in [0, n], given an action producing+-- uniformly-distributed words.+--+-- Each draw x is mapped to the high word of x * (n + 1); draws whose+-- low word falls below 2 ^ 64 mod (n + 1) are rejected, so that+-- every output is equally likely. Rejection is rare for small n, and+-- the remainder is only computed when it might occur.+upto :: Monad m => m Word64 -> Word64 -> m Word64+upto gen n+ | n == maxBound = gen+ | otherwise = do+ !x <- gen+ let !(W2 h l) = mul_wide x s+ if l >= s+ then pure h+ else reject (negate s `rem` s) h l+ where+ !s = n + 1+ reject !t = loop where+ loop !h !l+ | l >= t = pure h+ | otherwise = do+ !x <- gen+ let !(W2 nh nl) = mul_wide x s+ loop nh nl+{-# INLINE upto #-}++-- | 'upto' for a pure generator, given its step function.+upto_pure :: (g -> (Word64, g)) -> Word64 -> g -> (Word64, g)+upto_pure step n g0+ | n == maxBound = step g0+ | otherwise =+ let !(x, g1) = step g0+ !(W2 h l) = mul_wide x s+ in if l >= s+ then (h, g1)+ else reject (negate s `rem` s) h l g1+ where+ !s = n + 1+ reject !t = loop where+ loop !h !l !g+ | l >= t = (h, g)+ | otherwise =+ let !(x, ng) = step g+ !(W2 nh nl) = mul_wide x s+ in loop nh nl ng+{-# INLINE upto_pure #-}
+ lib/Crypto/CSPRNG.hs view
@@ -0,0 +1,324 @@+{-# OPTIONS_HADDOCK prune #-}+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Crypto.CSPRNG+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- A fast cryptographically-secure pseudorandom number generator:+-- [ChaCha8Rand](https://c2sp.org/chacha8rand), as used by Go's+-- @math/rand/v2@ and runtime.+--+-- The generator draws 992 bytes of output at a time from ChaCha8, and+-- rekeys itself from the next 32 bytes. Its key and output buffer are+-- erased as they are used, and the native ChaCha8 kernels (on aarch64+-- and x86_64) zero the stack they use, so that a later disclosure of+-- the process's memory reveals nothing about prior output. This+-- covers the generator's own state only: copies of seeds passed to it+-- and of output it returns are the caller's to manage, and on other+-- platforms the pure Haskell ChaCha8 fallback makes copies of keys and+-- keystream that the garbage collector manages and can't erase.+--+-- The generator is mutable, living in any 'PrimMonad' (e.g. 'IO' or+-- 'ST s'), and is not thread-safe. Use one generator per thread,+-- deriving more via 'split'.+--+-- A forked process (e.g. via @forkProcess@), or a restored virtual+-- machine snapshot, inherits a copy of the generator's state, and+-- then produces the same output as the original. Create a new+-- generator in such a child, e.g. via 'new_system', rather than using+-- an inherited one.++module Crypto.CSPRNG (+ -- * Generator+ Gen+ , new+ , new_system++ -- * Output+ , word64+ , word64_range+ , int_range+ , bytes++ -- * Splitting+ , split++ -- * Wiping+ , wipe+ ) where++import Control.Monad.Primitive (PrimMonad, PrimState, unsafeIOToPrim)+import Control.Monad.ST (ST)+import qualified Crypto.Cipher.ChaCha as ChaCha+import qualified Crypto.Entropy as Entropy+import qualified Crypto.Range as Range+import Data.Bits ((.&.), complement)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BI+import qualified Data.ByteString.Unsafe as BU+import qualified Data.Primitive.ByteArray as BA+import Data.Word (Word8, Word64, byteSwap64)+import Foreign.ForeignPtr (touchForeignPtr)+import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)+import Foreign.Marshal.Utils (copyBytes)+import Foreign.Ptr (Ptr, castPtr, plusPtr)+import GHC.ByteOrder (ByteOrder(..), targetByteOrder)+import GHC.Exts (RealWorld)++-- utils ----------------------------------------------------------------------++fi :: (Integral a, Num b) => a -> b+fi = fromIntegral+{-# INLINE fi #-}++-- convert between host and little-endian byte order+le64 :: Word64 -> Word64+le64 w = case targetByteOrder of+ LittleEndian -> w+ BigEndian -> byteSwap64 w+{-# INLINE le64 #-}++-- state ----------------------------------------------------------------------++-- a generator's state is a pinned byte array, laid out as:+--+-- offset size contents+-- 0 32 key for the next refill+-- 32 16 zero nonce (12 bytes used)+-- 48 8 position of the next unread byte of output+-- 64 1024 output (992 bytes) and next key (32 bytes)+--+-- all bytes of output, and all keys, are little-endian.++-- | A ChaCha8Rand generator.+newtype Gen s = Gen (BA.MutableByteArray s)++_KEY, _NONCE, _POS, _OUT, _SIZE, _AVAIL :: Int+_KEY = 0+_NONCE = 32+_POS = 48+_OUT = 64+_SIZE = 1088+_AVAIL = 992++alloc :: PrimMonad m => m (BA.MutableByteArray (PrimState m))+alloc = do+ st <- BA.newPinnedByteArray _SIZE+ BA.setByteArray st 0 _SIZE (0 :: Word8)+ pure st+{-# INLINE alloc #-}++read_pos :: PrimMonad m => BA.MutableByteArray (PrimState m) -> m Int+read_pos st = do+ !p <- BA.readByteArray st (_POS `quot` 8)+ pure $! fi (p :: Word64)+{-# INLINE read_pos #-}++write_pos :: PrimMonad m => BA.MutableByteArray (PrimState m) -> Int -> m ()+write_pos st p = BA.writeByteArray st (_POS `quot` 8) (fi p :: Word64)+{-# INLINE write_pos #-}++ptr :: BA.MutableByteArray s -> Int -> Ptr Word8+ptr st off = BA.mutableByteArrayContents st `plusPtr` off+{-# INLINE ptr #-}++-- ChaCha8Rand iteration: replace the output buffer with 992 fresh+-- bytes of output, and rekey.+refill :: PrimMonad m => BA.MutableByteArray (PrimState m) -> m ()+refill st = do+ -- sixteen ChaCha8 blocks, with counters 0 to 15, interleaved four+ -- at a time and with the final addition covering only the key+ unsafeIOToPrim $+ ChaCha._interleaved_into ChaCha.ChaCha8 ChaCha.FeedForwardKey+ (ptr st _KEY) 0 (ptr st _NONCE) (ptr st _OUT) 4+ -- the last 32 bytes of output are the next key+ BA.copyMutableByteArray st _KEY st (_OUT + _AVAIL) 32+ BA.setByteArray st (_OUT + _AVAIL) 32 (0 :: Word8)+ write_pos st 0+{-# INLINABLE refill #-}+{-# SPECIALIZE refill :: BA.MutableByteArray RealWorld -> IO () #-}+{-# SPECIALIZE refill :: BA.MutableByteArray s -> ST s () #-}++-- copy the next bytes of output to the destination, erasing them+fill+ :: PrimMonad m+ => BA.MutableByteArray (PrimState m)+ -> Ptr Word8+ -> Int+ -> m ()+fill st dst n = loop 0 where+ loop !off+ | off == n = pure ()+ | otherwise = do+ !pos <- read_pos st+ if pos == _AVAIL+ then do+ refill st+ loop off+ else do+ let !k = min (n - off) (_AVAIL - pos)+ !d = dst `plusPtr` off :: Ptr Word8+ BA.copyMutableByteArrayToPtr d st (_OUT + pos) k+ BA.setByteArray st (_OUT + pos) k (0 :: Word8)+ write_pos st (pos + k)+ loop (off + k)+{-# INLINABLE fill #-}++-- generator ------------------------------------------------------------------++-- | Create a generator from a 256-bit seed, returning 'Nothing' if the+-- seed is not exactly 32 bytes.+--+-- The seed must be secret and uniformly random; prefer 'new_system'+-- unless a reproducible stream is required.+--+-- >>> Just gen <- new "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"+-- >>> word64 gen+-- 13219109469176600229+new+ :: PrimMonad m+ => BS.ByteString+ -> m (Maybe (Gen (PrimState m)))+new seed@(BI.PS _ _ l)+ | l /= 32 = pure Nothing+ | otherwise = do+ st <- alloc+ unsafeIOToPrim $ BU.unsafeUseAsCString seed $ \sp ->+ copyBytes (ptr st _KEY) (castPtr sp) 32+ refill st+ pure $! Just (Gen st)+{-# INLINABLE new #-}+{-# SPECIALIZE new :: BS.ByteString -> IO (Maybe (Gen RealWorld)) #-}+{-# SPECIALIZE new :: BS.ByteString -> ST s (Maybe (Gen s)) #-}++-- | Create a generator seeded from the operating system's entropy+-- source, returning 'Nothing' if that fails.+--+-- >>> Just gen <- new_system+new_system :: IO (Maybe (Gen RealWorld))+new_system = do+ st <- alloc+ ok <- Entropy.fill (ptr st _KEY) 32+ if not ok+ then pure Nothing+ else do+ refill st+ pure $! Just (Gen st)++-- output ---------------------------------------------------------------------++-- | Generate a uniformly-distributed 'Word64' from the next eight+-- bytes of output, read in little-endian order. If a call to 'bytes'+-- has left the output out of eight-byte alignment, the bytes up to+-- the next boundary are discarded.+--+-- >>> Just gen <- new "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"+-- >>> word64 gen+-- 13219109469176600229+word64 :: PrimMonad m => Gen (PrimState m) -> m Word64+word64 (Gen st) = do+ !pos <- read_pos st+ let !aligned = (pos + 7) .&. complement 7+ !p <- if aligned + 8 > _AVAIL+ then do+ refill st+ pure 0+ else pure aligned+ let !ix = (_OUT + p) `quot` 8+ !w <- BA.readByteArray st ix+ BA.writeByteArray st ix (0 :: Word64)+ write_pos st (p + 8)+ pure $! le64 w+{-# INLINE word64 #-}++-- | Generate a uniformly-distributed 'Word64' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased; see 'int_range'.+--+-- >>> Just gen <- new "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"+-- >>> word64_range 1 6 gen+-- 5+word64_range+ :: PrimMonad m => Word64 -> Word64 -> Gen (PrimState m) -> m Word64+word64_range a b gen = do+ let !lo = min a b+ !hi = max a b+ !r <- Range.upto (word64 gen) (hi - lo)+ pure $! lo + r+{-# INLINE word64_range #-}++-- | Generate a uniformly-distributed 'Int' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased, via Lemire's method: an occasional draw is+-- rejected and replaced, so the number of words consumed, and the+-- running time, depend on the (discarded) rejected words.+--+-- >>> Just gen <- new "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"+-- >>> int_range (-10) 10 gen+-- 5+int_range :: PrimMonad m => Int -> Int -> Gen (PrimState m) -> m Int+int_range a b gen = do+ let !lo = min a b+ !hi = max a b+ !r <- Range.upto (word64 gen) (fi hi - fi lo)+ pure $! lo + fi r+{-# INLINE int_range #-}++-- | Generate the given number of bytes. Non-positive counts yield the+-- empty string.+--+-- >>> import qualified Data.ByteString.Base16 as B16+-- >>> Just gen <- new "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"+-- >>> fmap B16.encode (bytes 8 gen)+-- "a516463d06b673b7"+bytes :: PrimMonad m => Int -> Gen (PrimState m) -> m BS.ByteString+bytes n (Gen st)+ | n <= 0 = pure BS.empty+ | otherwise = do+ fp <- unsafeIOToPrim (BI.mallocByteString n)+ fill st (unsafeForeignPtrToPtr fp) n+ unsafeIOToPrim (touchForeignPtr fp)+ pure $! BI.BS fp n+{-# INLINABLE bytes #-}+{-# SPECIALIZE bytes :: Int -> Gen RealWorld -> IO BS.ByteString #-}+{-# SPECIALIZE bytes :: Int -> Gen s -> ST s BS.ByteString #-}++-- splitting ------------------------------------------------------------------++-- | Derive a new generator, seeded from the next 32 bytes of output.+--+-- The two generators' outputs are independent; use 'split' to give+-- each thread its own generator.+--+-- >>> Just gen <- new_system+-- >>> child <- split gen+split :: PrimMonad m => Gen (PrimState m) -> m (Gen (PrimState m))+split (Gen st) = do+ child <- alloc+ fill st (ptr child _KEY) 32+ refill child+ pure $! Gen child+{-# INLINABLE split #-}++-- wiping ---------------------------------------------------------------------++-- | Erase a generator's state.+--+-- Call this when finished with a generator, to wipe its key and any+-- unused output from memory. The generator must not be used+-- afterwards: it would then produce output from an all-zero key,+-- which is predictable.+--+-- >>> Just gen <- new_system+-- >>> out <- bytes 32 gen+-- >>> wipe gen+wipe :: PrimMonad m => Gen (PrimState m) -> m ()+wipe (Gen st) = do+ BA.setByteArray st 0 _SIZE (0 :: Word8)+ write_pos st _AVAIL+{-# INLINE wipe #-}
+ lib/Crypto/Entropy.hs view
@@ -0,0 +1,30 @@+{-# OPTIONS_HADDOCK hide #-}++-- |+-- Module: Crypto.Entropy+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- Entropy from the operating system, via getentropy(3).++module Crypto.Entropy (+ fill+ ) where++import Data.Word (Word8)+import Foreign.C.Types (CInt(..), CSize(..))+import Foreign.Ptr (Ptr)++-- getentropy may block until the system CSPRNG is seeded, so the call+-- is safe rather than unsafe+foreign import ccall safe "getentropy"+ c_getentropy :: Ptr Word8 -> CSize -> IO CInt++-- | Fill the destination with the given number of bytes (at most 256)+-- of entropy from the operating system, returning 'False' on+-- failure.+fill :: Ptr Word8 -> Int -> IO Bool+fill dst n = do+ r <- c_getentropy dst (fromIntegral n)+ pure $! r == 0
+ lib/Crypto/PRNG.hs view
@@ -0,0 +1,228 @@+{-# OPTIONS_HADDOCK prune #-}+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Crypto.PRNG+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- A fast, non-cryptographic pseudorandom number generator:+-- [xoshiro256++](https://prng.di.unimi.it/), by David Blackman and+-- Sebastiano Vigna.+--+-- The generator is mutable, living in any 'PrimMonad' (e.g. 'IO' or+-- 'ST s'), and is not thread-safe. Use one generator per thread,+-- deriving non-overlapping streams via 'copy' and 'jump' or+-- 'long_jump'.+--+-- Its output is predictable from a small sample; use "Crypto.CSPRNG"+-- for anything that must be unpredictable. "Crypto.PRNG.Pure" offers+-- the same generator, with the same output, as an immutable value.++module Crypto.PRNG (+ -- * Generator+ Gen+ , new+ , new_system+ , from_state++ -- * Output+ , word64+ , word64_range+ , int_range+ , double++ -- * Streams+ , copy+ , jump+ , long_jump+ ) where++import Control.Monad.Primitive (PrimMonad, PrimState)+import Control.Monad.ST (ST)+import qualified Crypto.Range as Range+import Crypto.Xoshiro (S(..))+import qualified Crypto.Xoshiro as X+import Data.Bits ((.>>.))+import qualified Data.Primitive.PrimArray as PA+import Data.Word (Word64)+import GHC.Exts (RealWorld)++-- utils ----------------------------------------------------------------------++fi :: (Integral a, Num b) => a -> b+fi = fromIntegral+{-# INLINE fi #-}++-- generator ------------------------------------------------------------------++-- | A xoshiro256++ generator.+newtype Gen s = Gen (PA.MutablePrimArray s Word64)++read_s :: PrimMonad m => Gen (PrimState m) -> m S+read_s (Gen g) = do+ !s0 <- PA.readPrimArray g 0+ !s1 <- PA.readPrimArray g 1+ !s2 <- PA.readPrimArray g 2+ !s3 <- PA.readPrimArray g 3+ pure $! S s0 s1 s2 s3+{-# INLINE read_s #-}++write_s :: PrimMonad m => Gen (PrimState m) -> S -> m ()+write_s (Gen g) (S s0 s1 s2 s3) = do+ PA.writePrimArray g 0 s0+ PA.writePrimArray g 1 s1+ PA.writePrimArray g 2 s2+ PA.writePrimArray g 3 s3+{-# INLINE write_s #-}++alloc :: PrimMonad m => S -> m (Gen (PrimState m))+alloc s = do+ g <- fmap Gen (PA.newPrimArray 4)+ write_s g s+ pure g+{-# INLINE alloc #-}++-- | Create a generator from a 64-bit seed, expanded to the full+-- 256-bit state via splitmix64, per the xoshiro authors'+-- recommendation.+--+-- >>> gen <- new 42+-- >>> word64 gen+-- 15021278609987233951+new :: PrimMonad m => Word64 -> m (Gen (PrimState m))+new = alloc . X.seed+{-# INLINABLE new #-}+{-# SPECIALIZE new :: Word64 -> IO (Gen RealWorld) #-}+{-# SPECIALIZE new :: Word64 -> ST s (Gen s) #-}++-- | Create a generator from the given 256-bit state, returning+-- 'Nothing' if it is all zero.+--+-- >>> Just gen <- from_state 1 2 3 4+-- >>> word64 gen+-- 41943041+from_state+ :: PrimMonad m+ => Word64+ -> Word64+ -> Word64+ -> Word64+ -> m (Maybe (Gen (PrimState m)))+from_state s0 s1 s2 s3 = traverse alloc (X.from_words s0 s1 s2 s3)+{-# INLINABLE from_state #-}++-- | Create a generator with a 256-bit state drawn from the operating+-- system's entropy source, returning 'Nothing' if that fails.+--+-- >>> Just gen <- new_system+new_system :: IO (Maybe (Gen RealWorld))+new_system = X.system >>= traverse alloc++-- output ---------------------------------------------------------------------++-- | Generate a uniformly-distributed 'Word64'.+--+-- >>> Just gen <- from_state 1 2 3 4+-- >>> word64 gen+-- 41943041+-- >>> word64 gen+-- 58720359+word64 :: PrimMonad m => Gen (PrimState m) -> m Word64+word64 gen = do+ !s <- read_s gen+ write_s gen (X.next s)+ pure $! X.output s+{-# INLINE word64 #-}++-- | Generate a uniformly-distributed 'Word64' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased; see 'int_range'.+--+-- >>> gen <- new 42+-- >>> word64_range 1 6 gen+-- 5+word64_range+ :: PrimMonad m => Word64 -> Word64 -> Gen (PrimState m) -> m Word64+word64_range a b gen = do+ let !lo = min a b+ !hi = max a b+ !r <- Range.upto (word64 gen) (hi - lo)+ pure $! lo + r+{-# INLINE word64_range #-}++-- | Generate a uniformly-distributed 'Int' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased, via Lemire's method: an occasional draw is+-- rejected and replaced, so the number of words consumed varies.+--+-- >>> gen <- new 42+-- >>> int_range (-10) 10 gen+-- 7+int_range :: PrimMonad m => Int -> Int -> Gen (PrimState m) -> m Int+int_range a b gen = do+ let !lo = min a b+ !hi = max a b+ !r <- Range.upto (word64 gen) (fi hi - fi lo)+ pure $! lo + fi r+{-# INLINE int_range #-}++-- | Generate a uniformly-distributed 'Double' in [0, 1), from the+-- upper 53 bits of a 'Word64'.+--+-- >>> Just gen <- from_state 1 2 3 4+-- >>> double gen+-- 2.2737367544323206e-12+double :: PrimMonad m => Gen (PrimState m) -> m Double+double gen = do+ !w <- word64 gen+ -- 2 ^ -53+ pure $! fi (w .>>. 11) * 1.1102230246251565e-16+{-# INLINE double #-}++-- streams --------------------------------------------------------------------++-- | Copy a generator. The copy produces the same output as the+-- original, independently of it.+--+-- >>> gen <- new 42+-- >>> cop <- copy gen+-- >>> (==) <$> word64 gen <*> word64 cop+-- True+copy :: PrimMonad m => Gen (PrimState m) -> m (Gen (PrimState m))+copy gen = read_s gen >>= alloc+{-# INLINE copy #-}++-- | Advance a generator by 2 ^ 128 outputs.+--+-- Copying and then jumping repeatedly yields up to 2 ^ 128+-- non-overlapping streams of 2 ^ 128 outputs each, e.g. one per+-- thread.+--+-- >>> gen <- new 42+-- >>> one <- copy gen+-- >>> jump gen+-- >>> two <- copy gen+-- >>> jump gen+jump :: PrimMonad m => Gen (PrimState m) -> m ()+jump gen = do+ !s <- read_s gen+ write_s gen (X.jump s)+{-# INLINABLE jump #-}++-- | Advance a generator by 2 ^ 192 outputs.+--+-- Copying and long-jumping repeatedly yields up to 2 ^ 64 starting+-- points, from each of which 'jump' yields 2 ^ 64 non-overlapping+-- streams.+--+-- >>> gen <- new 42+-- >>> long_jump gen+long_jump :: PrimMonad m => Gen (PrimState m) -> m ()+long_jump gen = do+ !s <- read_s gen+ write_s gen (X.long_jump s)+{-# INLINABLE long_jump #-}
+ lib/Crypto/PRNG/Pure.hs view
@@ -0,0 +1,179 @@+{-# OPTIONS_HADDOCK prune #-}+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Crypto.PRNG.Pure+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- A fast, non-cryptographic pseudorandom number generator,+-- [xoshiro256++](https://prng.di.unimi.it/), as an immutable value.+--+-- This is the generator of "Crypto.PRNG", with the same output for the+-- same seed, but threaded through code explicitly: each function+-- returns its result along with the next generator. Generators are+-- values, so using one twice yields the same output twice; derive+-- non-overlapping streams via 'jump' or 'long_jump'.+--+-- When these functions are inlined into a strict loop, GHC keeps the+-- generator's state in registers and allocates nothing, which is+-- several times faster than the mutable generator. Bind results with+-- @case@ or bang patterns rather than lazy @let@s. Called where GHC+-- can't inline or specialise them, e.g. via a lazy fold or an+-- unspecialised class method, each draw allocates, and the mutable+-- "Crypto.PRNG" may be faster.+--+-- Its output is predictable from a small sample; use "Crypto.CSPRNG"+-- for anything that must be unpredictable.++module Crypto.PRNG.Pure (+ -- * Generator+ Gen+ , Draw(..)+ , new+ , new_system+ , from_state++ -- * Output+ , word64+ , word64_range+ , int_range+ , double++ -- * Streams+ , jump+ , long_jump+ ) where++import qualified Crypto.Range as Range+import qualified Crypto.Xoshiro as X+import Data.Bits ((.>>.))+import Data.Word (Word64)++-- utils ----------------------------------------------------------------------++fi :: (Integral a, Num b) => a -> b+fi = fromIntegral+{-# INLINE fi #-}++-- generator ------------------------------------------------------------------++-- | A xoshiro256++ generator.+newtype Gen = Gen X.S++-- | A generated value, along with the next generator.+data Draw a = Draw !a {-# UNPACK #-} !Gen++-- | Create a generator from a 64-bit seed, expanded to the full+-- 256-bit state via splitmix64, per the xoshiro authors'+-- recommendation.+--+-- >>> let Draw w _ = word64 (new 42)+-- >>> w+-- 15021278609987233951+new :: Word64 -> Gen+new = Gen . X.seed+{-# INLINE new #-}++-- | Create a generator from the given 256-bit state, returning+-- 'Nothing' if it is all zero.+--+-- >>> let Just gen = from_state 1 2 3 4+-- >>> let Draw w _ = word64 gen+-- >>> w+-- 41943041+from_state :: Word64 -> Word64 -> Word64 -> Word64 -> Maybe Gen+from_state s0 s1 s2 s3 = fmap Gen (X.from_words s0 s1 s2 s3)+{-# INLINE from_state #-}++-- | Create a generator with a 256-bit state drawn from the operating+-- system's entropy source, returning 'Nothing' if that fails.+--+-- >>> Just gen <- new_system+new_system :: IO (Maybe Gen)+new_system = fmap (fmap Gen) X.system++-- output ---------------------------------------------------------------------++-- | Generate a uniformly-distributed 'Word64'.+--+-- >>> let Just gen = from_state 1 2 3 4+-- >>> let Draw a g1 = word64 gen+-- >>> let Draw b _ = word64 g1+-- >>> (a, b)+-- (41943041,58720359)+word64 :: Gen -> Draw Word64+word64 (Gen s) = Draw (X.output s) (Gen (X.next s))+{-# INLINE word64 #-}++-- | Generate a uniformly-distributed 'Word64' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased; see 'int_range'.+--+-- >>> let Draw w _ = word64_range 1 6 (new 42)+-- >>> w+-- 5+word64_range :: Word64 -> Word64 -> Gen -> Draw Word64+word64_range a b (Gen s) =+ let !lo = min a b+ !hi = max a b+ !(r, ns) = Range.upto_pure step (hi - lo) s+ in Draw (lo + r) (Gen ns)+{-# INLINE word64_range #-}++-- | Generate a uniformly-distributed 'Int' in the given inclusive+-- range, whose bounds may be given in either order.+--+-- The result is unbiased, via Lemire's method: an occasional draw is+-- rejected and replaced, so the number of words consumed varies.+--+-- >>> let Draw x _ = int_range (-10) 10 (new 42)+-- >>> x+-- 7+int_range :: Int -> Int -> Gen -> Draw Int+int_range a b (Gen s) =+ let !lo = min a b+ !hi = max a b+ !(r, ns) = Range.upto_pure step (fi hi - fi lo) s+ in Draw (lo + fi r) (Gen ns)+{-# INLINE int_range #-}++-- | Generate a uniformly-distributed 'Double' in [0, 1), from the+-- upper 53 bits of a 'Word64'.+--+-- >>> let Just gen = from_state 1 2 3 4+-- >>> let Draw d _ = double gen+-- >>> d+-- 2.2737367544323206e-12+double :: Gen -> Draw Double+double gen = case word64 gen of+ -- 2 ^ -53+ Draw w ng -> Draw (fi (w .>>. 11) * 1.1102230246251565e-16) ng+{-# INLINE double #-}++-- one step of the generator, for 'Range.upto_pure'+step :: X.S -> (Word64, X.S)+step s = (X.output s, X.next s)+{-# INLINE step #-}++-- streams --------------------------------------------------------------------++-- | Advance a generator by 2 ^ 128 outputs.+--+-- Repeated jumps yield up to 2 ^ 128 non-overlapping streams of+-- 2 ^ 128 outputs each, e.g. one per thread.+--+-- >>> let gens = take 4 (iterate jump (new 42))+jump :: Gen -> Gen+jump (Gen s) = Gen (X.jump s)++-- | Advance a generator by 2 ^ 192 outputs.+--+-- Repeated long jumps yield up to 2 ^ 64 starting points, from each+-- of which 'jump' yields 2 ^ 64 non-overlapping streams.+--+-- >>> let starts = take 4 (iterate long_jump (new 42))+long_jump :: Gen -> Gen+long_jump (Gen s) = Gen (X.long_jump s)
+ lib/Crypto/Xoshiro.hs view
@@ -0,0 +1,135 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Crypto.Xoshiro+-- Copyright: (c) 2026 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- The xoshiro256++ state, step function, scrambler, jumps, and+-- seeding, shared by the mutable and pure generators.++module Crypto.Xoshiro (+ S(..)+ , output+ , next+ , jump+ , long_jump+ , seed+ , from_words+ , system+ ) where++import qualified Crypto.Entropy as Entropy+import Data.Bits ((.|.), (.^.), (.<<.), (.>>.))+import qualified Data.Bits as B+import Data.Word (Word64)+import Foreign.Marshal.Alloc (allocaBytes)+import Foreign.Ptr (Ptr, castPtr)+import Foreign.Storable (peekElemOff)++rotl :: Word64 -> Int -> Word64+rotl x k = (x .<<. k) .|. (x .>>. (64 - k))+{-# INLINE rotl #-}++-- | A xoshiro256++ state, which must not be all zero.+data S = S+ {-# UNPACK #-} !Word64+ {-# UNPACK #-} !Word64+ {-# UNPACK #-} !Word64+ {-# UNPACK #-} !Word64++-- | The ++ scrambler: the output for a state.+output :: S -> Word64+output (S s0 _ _ s3) = rotl (s0 + s3) 23 + s0+{-# INLINE output #-}++-- | The linear engine: the next state.+next :: S -> S+next (S s0 s1 s2 s3) =+ let !t = s1 .<<. 17+ !a2 = s2 .^. s0+ !a3 = s3 .^. s1+ !b1 = s1 .^. a2+ !b0 = s0 .^. a3+ !b2 = a2 .^. t+ !b3 = rotl a3 45+ in S b0 b1 b2 b3+{-# INLINE next #-}++xor_s :: S -> S -> S+xor_s (S a0 a1 a2 a3) (S b0 b1 b2 b3) =+ S (a0 .^. b0) (a1 .^. b1) (a2 .^. b2) (a3 .^. b3)+{-# INLINE xor_s #-}++data P = P !S !S++-- advance the state by the jump polynomial with the given coefficients+leap :: Word64 -> Word64 -> Word64 -> Word64 -> S -> S+leap j0 j1 j2 j3 s0 =+ let !(P acc _) =+ coef j3 (coef j2 (coef j1 (coef j0 (P (S 0 0 0 0) s0))))+ in acc+ where+ coef :: Word64 -> P -> P+ coef j = loop 0 where+ loop :: Int -> P -> P+ loop !b p@(P acc s)+ | b == 64 = p+ | otherwise =+ let !nacc | B.testBit j b = xor_s acc s+ | otherwise = acc+ in loop (b + 1) (P nacc (next s))++-- | Advance a state by 2 ^ 128 steps.+jump :: S -> S+jump = leap 0x180ec6d33cfd0aba 0xd5a61266f0c9392c+ 0xa9582618e03fc9aa 0x39abdc4529b1661c++-- | Advance a state by 2 ^ 192 steps.+long_jump :: S -> S+long_jump = leap 0x76e15d3efefdcbbf 0xc5004e441c522fb3+ 0x77710069854ee241 0x39109bb02acbe635++data SM = SM {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64++splitmix64 :: Word64 -> SM+splitmix64 x =+ let !nx = x + 0x9e3779b97f4a7c15+ !z0 = (nx .^. (nx .>>. 30)) * 0xbf58476d1ce4e5b9+ !z1 = (z0 .^. (z0 .>>. 27)) * 0x94d049bb133111eb+ in SM nx (z1 .^. (z1 .>>. 31))+{-# INLINE splitmix64 #-}++-- | A state expanded from a 64-bit seed via splitmix64, per the+-- xoshiro authors' recommendation. It is never all zero.+seed :: Word64 -> S+seed x =+ let !(SM x0 s0) = splitmix64 x+ !(SM x1 s1) = splitmix64 x0+ !(SM x2 s2) = splitmix64 x1+ !(SM _ s3) = splitmix64 x2+ in S s0 s1 s2 s3++-- | A state from the given words, unless they are all zero.+from_words :: Word64 -> Word64 -> Word64 -> Word64 -> Maybe S+from_words s0 s1 s2 s3+ | s0 .|. s1 .|. s2 .|. s3 == 0 = Nothing+ | otherwise = Just (S s0 s1 s2 s3)+{-# INLINE from_words #-}++-- | A state drawn from the operating system's entropy source, unless+-- that fails.+system :: IO (Maybe S)+system = allocaBytes 32 $ \buf -> do+ ok <- Entropy.fill buf 32+ if not ok+ then pure Nothing+ else do+ let p = castPtr buf :: Ptr Word64+ s0 <- peekElemOff p 0+ s1 <- peekElemOff p 1+ s2 <- peekElemOff p 2+ s3 <- peekElemOff p 3+ pure $! from_words s0 s1 s2 s3
+ ppad-random.cabal view
@@ -0,0 +1,106 @@+cabal-version: 3.0+name: ppad-random+version: 0.1.0+synopsis: Fast random number generators.+license: MIT+license-file: LICENSE+author: Jared Tobin+maintainer: jared@ppad.tech+category: Cryptography, Random+build-type: Simple+tested-with: GHC == 9.10.3+extra-doc-files: CHANGELOG+description:+ Fast random number generators: the xoshiro256++ PRNG, as a mutable+ generator or a pure value, and the ChaCha8Rand CSPRNG, with unbiased+ bounded integers and seeding from the operating system.++flag llvm+ description: Use GHC's LLVM backend.+ default: False+ manual: True++source-repository head+ type: git+ location: git.ppad.tech/random.git++library+ default-language: Haskell2010+ hs-source-dirs: lib, internal+ ghc-options:+ -Wall+ if flag(llvm)+ ghc-options: -fllvm -O2+ exposed-modules:+ Crypto.CSPRNG+ , Crypto.PRNG+ , Crypto.PRNG.Pure+ other-modules:+ Crypto.Entropy+ , Crypto.Range+ , Crypto.Xoshiro+ build-depends:+ base >= 4.17 && < 5+ , bytestring >= 0.11 && < 0.13+ , ppad-chacha >= 0.3.4 && < 0.4+ , primitive >= 0.8 && < 0.10++test-suite random-tests+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: test, internal+ main-is: Main.hs+ other-modules:+ Crypto.Range+ , Vectors++ ghc-options:+ -rtsopts -Wall -O2++ build-depends:+ base+ , bytestring+ , ppad-base16+ , ppad-random+ , tasty+ , tasty-hunit++benchmark random-bench+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: bench+ main-is: Main.hs++ ghc-options:+ -rtsopts -O2 -Wall+ if flag(llvm)+ ghc-options: -fllvm++ build-depends:+ base+ , bytestring+ , criterion+ , crypton+ , entropy+ , mwc-random+ , pcg-random+ , ppad-random+ , random+ , splitmix++benchmark random-weigh+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: bench+ main-is: Weight.hs++ ghc-options:+ -rtsopts -O2 -Wall+ if flag(llvm)+ ghc-options: -fllvm++ build-depends:+ base+ , bytestring+ , ppad-random+ , weigh
+ test/Main.hs view
@@ -0,0 +1,457 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Monad (replicateM)+import Control.Monad.ST (runST)+import qualified Crypto.CSPRNG as CSPRNG+import qualified Crypto.PRNG as PRNG+import qualified Crypto.PRNG.Pure as Pure+import qualified Crypto.Range as Range+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as B16+import Data.Bits ((.<<.), (.|.))+import Data.Foldable (for_)+import qualified Data.List as L+import Data.Word (Word64)+import GHC.Exts (RealWorld)+import Test.Tasty+import qualified Test.Tasty.HUnit as H+import Vectors (chacha8rand_seed, chacha8rand_hex, chacha8rand_words)++main :: IO ()+main = defaultMain $ testGroup "ppad-random" [+ testGroup "Crypto.PRNG" [+ prng_reference+ , prng_jump+ , prng_long_jump+ , prng_new+ , prng_zero_state+ , prng_copy+ , prng_double+ , prng_system+ ]+ , testGroup "Crypto.PRNG.Pure" [+ pure_reference+ , pure_jumps+ , pure_new+ , pure_zero_state+ , pure_agrees+ ]+ , testGroup "ranges" [+ mul_wide+ , prng_word64_range+ , prng_int_range_bounds+ , prng_int_range_order+ , prng_int_range_full+ , prng_uniformity+ , csprng_word64_range+ ]+ , testGroup "Crypto.CSPRNG" [+ csprng_words+ , csprng_bytes+ , csprng_chunks+ , csprng_alignment+ , csprng_seed_length+ , csprng_split+ , csprng_st+ , csprng_system+ ]+ ]++-- utilities ------------------------------------------------------------------++hex :: BS.ByteString -> BS.ByteString+hex bs = case B16.decode bs of+ Nothing -> error "ppad-random (test): invalid hex"+ Just x -> x++prng_state+ :: Word64 -> Word64 -> Word64 -> Word64 -> IO (PRNG.Gen RealWorld)+prng_state s0 s1 s2 s3 = do+ mg <- PRNG.from_state s0 s1 s2 s3+ case mg of+ Nothing -> H.assertFailure "invalid state"+ Just g -> pure g++csprng_seeded :: BS.ByteString -> IO (CSPRNG.Gen RealWorld)+csprng_seeded seed = do+ mg <- CSPRNG.new seed+ case mg of+ Nothing -> H.assertFailure "invalid seed"+ Just g -> pure g++-- little-endian word at the given index of a bytestring+le64_at :: BS.ByteString -> Int -> Word64+le64_at bs k =+ BS.foldr (\b acc -> (acc .<<. 8) .|. fromIntegral b) 0+ (BS.take 8 (BS.drop (8 * k) bs))++-- xoshiro256++ ---------------------------------------------------------------++-- the vectors below are from Blackman and Vigna's reference+-- implementation, as published by rand_xoshiro (reference outputs)+-- or generated with it (jumps and seeding)++prng_reference :: TestTree+prng_reference = H.testCase "reference output" $ do+ g <- prng_state 1 2 3 4+ out <- replicateM 10 (PRNG.word64 g)+ H.assertEqual mempty [+ 41943041, 58720359, 3588806011781223, 3591011842654386+ , 9228616714210784205, 9973669472204895162, 14011001112246962877+ , 12406186145184390807, 15849039046786891736, 10450023813501588000+ ] out++prng_jump :: TestTree+prng_jump = H.testCase "jump" $ do+ g <- prng_state 1 2 3 4+ PRNG.jump g+ out <- replicateM 4 (PRNG.word64 g)+ H.assertEqual mempty [+ 0xec879073673df437, 0x20d212a39aca1eaa+ , 0xc19d712a27e40f57, 0x6ff0e08dc71026a1+ ] out++prng_long_jump :: TestTree+prng_long_jump = H.testCase "long_jump" $ do+ g <- prng_state 1 2 3 4+ PRNG.long_jump g+ out <- replicateM 4 (PRNG.word64 g)+ H.assertEqual mempty [+ 0xb5c4ea370b330bf5, 0x5173cc693c0fa533+ , 0x1dc5df0151f7b491, 0xe7b055cfeabc4661+ ] out++prng_new :: TestTree+prng_new = H.testCase "new (splitmix64 seeding)" $+ for_ cases $ \(seed, expected) -> do+ g <- PRNG.new seed+ out <- replicateM 4 (PRNG.word64 g)+ H.assertEqual (show seed) expected out+ where+ cases = [+ (0, [ 0x53175d61490b23df, 0x61da6f3dc380d507+ , 0x5c0fdf91ec9a7bfc, 0x02eebf8c3bbe5e1a ])+ , (42, [ 0xd0764d4f4476689f, 0x519e4174576f3791+ , 0xfbe07cfb0c24ed8c, 0xb37d9f600cd835b8 ])+ , (maxBound, [ 0x56ccf8ce948e27b2, 0xe68588432e5a5b90+ , 0xe3e9b5a48119ca8b, 0x460f19495532ae73 ])+ ]++prng_zero_state :: TestTree+prng_zero_state = H.testCase "from_state rejects the zero state" $ do+ mg <- PRNG.from_state 0 0 0 0+ case mg of+ Nothing -> pure ()+ Just _ -> H.assertFailure "accepted zero state"++prng_copy :: TestTree+prng_copy = H.testCase "copy" $ do+ g <- PRNG.new 42+ _ <- PRNG.word64 g+ c <- PRNG.copy g+ a <- replicateM 8 (PRNG.word64 g)+ b <- replicateM 8 (PRNG.word64 c)+ H.assertEqual "same stream" a b+ -- jumping the copy leaves the original alone+ PRNG.jump c+ x <- replicateM 8 (PRNG.word64 g)+ ref <- PRNG.new 42+ y <- fmap (drop 9) (replicateM 17 (PRNG.word64 ref))+ H.assertEqual "original unaffected" y x++prng_double :: TestTree+prng_double = H.testCase "double" $ do+ g <- PRNG.new 42+ c <- PRNG.copy g+ ds <- replicateM 1000 (PRNG.double g)+ ws <- replicateM 1000 (PRNG.word64 c)+ H.assertBool "range" (all (\d -> d >= 0 && d < 1) ds)+ H.assertEqual "upper 53 bits"+ (fmap (\w -> fromIntegral (w `div` 2048) / 2 ^ (53 :: Int)) ws) ds++prng_system :: TestTree+prng_system = H.testCase "new_system" $ do+ ma <- PRNG.new_system+ mb <- PRNG.new_system+ case (ma, mb) of+ (Just a, Just b) -> do+ xs <- replicateM 4 (PRNG.word64 a)+ ys <- replicateM 4 (PRNG.word64 b)+ H.assertBool "distinct" (xs /= ys)+ _ -> H.assertFailure "no entropy"++-- chacha8rand ----------------------------------------------------------------++-- the vectors are the sample output from the C2SP specification++csprng_words :: TestTree+csprng_words = H.testCase "sample output (word64)" $ do+ g <- csprng_seeded chacha8rand_seed+ out <- replicateM (length chacha8rand_words) (CSPRNG.word64 g)+ H.assertEqual mempty chacha8rand_words out++csprng_bytes :: TestTree+csprng_bytes = H.testCase "sample output (bytes)" $ do+ g <- csprng_seeded chacha8rand_seed+ out <- CSPRNG.bytes 2976 g+ H.assertEqual mempty (hex chacha8rand_hex) out++-- requests of assorted sizes, straddling refills, concatenate to the+-- same stream+csprng_chunks :: TestTree+csprng_chunks = H.testCase "sample output (chunked bytes)" $ do+ g <- csprng_seeded chacha8rand_seed+ let sizes = [0, 1, 7, 13, 100, 900, 992, 1, 962, -3]+ out <- traverse (\n -> CSPRNG.bytes n g) sizes+ H.assertEqual mempty (hex chacha8rand_hex) (BS.concat out)++-- after an unaligned 'bytes', 'word64' skips to the next eight-byte+-- boundary, including across a refill+csprng_alignment :: TestTree+csprng_alignment = H.testCase "word64 alignment" $ do+ let stream = hex chacha8rand_hex+ g <- csprng_seeded chacha8rand_seed+ _ <- CSPRNG.bytes 3 g+ a <- CSPRNG.word64 g+ H.assertEqual "within buffer" (le64_at stream 1) a+ _ <- CSPRNG.bytes (992 - 16 - 1) g+ b <- CSPRNG.word64 g+ H.assertEqual "across refill" (le64_at stream 124) b++csprng_seed_length :: TestTree+csprng_seed_length = H.testCase "new rejects bad seed lengths" $+ for_ [0, 31, 33] $ \l -> do+ mg <- CSPRNG.new (BS.replicate l 0)+ case mg of+ Nothing -> pure ()+ Just _ -> H.assertFailure ("accepted length " <> show l)++-- a child is seeded from the parent's next 32 bytes, which the parent+-- then skips+csprng_split :: TestTree+csprng_split = H.testCase "split" $ do+ let stream = hex chacha8rand_hex+ g <- csprng_seeded chacha8rand_seed+ c <- CSPRNG.split g+ r <- csprng_seeded (BS.take 32 stream)+ x <- CSPRNG.bytes 2000 c+ y <- CSPRNG.bytes 2000 r+ H.assertEqual "child" y x+ z <- CSPRNG.bytes 100 g+ H.assertEqual "parent" (BS.take 100 (BS.drop 32 stream)) z++csprng_st :: TestTree+csprng_st = H.testCase "ST" $ do+ let out = runST $ do+ mg <- CSPRNG.new chacha8rand_seed+ case mg of+ Nothing -> pure []+ Just g -> replicateM 200 (CSPRNG.word64 g)+ H.assertEqual mempty (take 200 chacha8rand_words) out++csprng_system :: TestTree+csprng_system = H.testCase "new_system" $ do+ ma <- CSPRNG.new_system+ mb <- CSPRNG.new_system+ case (ma, mb) of+ (Just a, Just b) -> do+ x <- CSPRNG.bytes 32 a+ y <- CSPRNG.bytes 32 b+ H.assertBool "distinct" (x /= y)+ _ -> H.assertFailure "no entropy"++-- ranges ---------------------------------------------------------------------++-- reference model of Lemire's method over a stream of words, using+-- exact integer arithmetic: a draw w is accepted iff the low word of+-- w * s is at least 2 ^ 64 mod s, yielding the high word+ref_ranges :: [Word64] -> [Word64] -> Maybe [Word64]+ref_ranges [] _ = Just []+ref_ranges (n : ns) ws+ | n == maxBound = case ws of+ [] -> Nothing+ (w : rest) -> fmap (w :) (ref_ranges ns rest)+ | otherwise = draw ws+ where+ s = toInteger n + 1+ t = 2 ^ (64 :: Int) `mod` s+ draw [] = Nothing+ draw (w : rest) =+ let m = toInteger w * s+ in if m `mod` 2 ^ (64 :: Int) >= t+ then fmap (fromInteger (m `div` 2 ^ (64 :: Int)) :)+ (ref_ranges ns rest)+ else draw rest++-- bounds including some with rejection probabilities near 1 / 2+range_bounds :: [Word64]+range_bounds = concat . replicate 20 $ [+ 0, 1, 5, 6, 100, 2 ^ (32 :: Int), 2 ^ (63 :: Int)+ , 2 ^ (63 :: Int) + 12345, maxBound - 1, maxBound+ ]++mul_wide :: TestTree+mul_wide = H.testCase "mul_wide (native and limbs)" $+ for_ edges $ \a -> for_ edges $ \b -> do+ let p = toInteger a * toInteger b+ ex = Range.W2 (fromInteger (p `div` 2 ^ (64 :: Int)))+ (fromInteger (p `mod` 2 ^ (64 :: Int)))+ H.assertEqual (show (a, b)) ex (Range.mul_wide a b)+ H.assertEqual (show (a, b)) ex (Range._mul_wide_limbs a b)+ where+ edges = [ 0, 1, 2, 0xffffffff, 0x100000000, 0x7fffffffffffffff+ , 0x8000000000000000, 0xfffffffffffffffe, maxBound+ , 0xd0764d4f4476689f, 0x519e4174576f3791 ]++prng_word64_range :: TestTree+prng_word64_range = H.testCase "PRNG word64_range (reference model)" $ do+ g <- PRNG.new 42+ c <- PRNG.copy g+ out <- traverse (\n -> PRNG.word64_range 0 n g) range_bounds+ ws <- replicateM 1000 (PRNG.word64 c)+ H.assertEqual mempty (Just out) (ref_ranges range_bounds ws)+ -- an offset range is the same draw, shifted+ g1 <- PRNG.new 7+ g2 <- PRNG.copy g1+ x <- PRNG.word64_range 1000 1100 g1+ y <- PRNG.word64_range 0 100 g2+ H.assertEqual "offset" (y + 1000) x++prng_int_range_bounds :: TestTree+prng_int_range_bounds = H.testCase "PRNG int_range (bounds)" $ do+ g <- PRNG.new 42+ for_ cases $ \(lo, hi) -> do+ xs <- replicateM 200 (PRNG.int_range lo hi g)+ H.assertBool (show (lo, hi)) (all (\x -> x >= lo && x <= hi) xs)+ where+ cases = [ (-10, 10), (5, 5), (-1, 0), (minBound, minBound + 3)+ , (maxBound - 3, maxBound), (minBound, -1), (0, maxBound) ]++prng_int_range_order :: TestTree+prng_int_range_order = H.testCase "PRNG int_range (bound order)" $ do+ g <- PRNG.new 42+ c <- PRNG.copy g+ xs <- replicateM 100 (PRNG.int_range (-10) 10 g)+ ys <- replicateM 100 (PRNG.int_range 10 (-10) c)+ H.assertEqual mempty xs ys++prng_int_range_full :: TestTree+prng_int_range_full = H.testCase "PRNG int_range (full range)" $ do+ g <- PRNG.new 42+ c <- PRNG.copy g+ xs <- replicateM 100 (PRNG.int_range minBound maxBound g)+ ws <- replicateM 100 (PRNG.word64 c)+ H.assertEqual mempty (fmap (\w -> minBound + fromIntegral w) ws) xs++-- chi-squared over six outcomes from a fixed seed; 20.5 is the 0.999+-- quantile for five degrees of freedom+prng_uniformity :: TestTree+prng_uniformity = H.testCase "PRNG int_range (uniformity)" $ do+ g <- PRNG.new 42+ xs <- replicateM 60000 (PRNG.int_range 1 6 g)+ let counts = fmap length (L.group (L.sort xs))+ chi = sum [ (fromIntegral k - 10000) ^ (2 :: Int) / 10000+ | k <- counts ] :: Double+ H.assertEqual "outcomes" 6 (length counts)+ H.assertBool ("chi-squared " <> show chi) (chi < 20.5)++-- over the known C2SP stream+csprng_word64_range :: TestTree+csprng_word64_range = H.testCase "CSPRNG word64_range (reference model)" $ do+ g <- csprng_seeded chacha8rand_seed+ let bounds = take 60 range_bounds+ out <- traverse (\n -> CSPRNG.word64_range 0 n g) bounds+ H.assertEqual mempty (Just out) (ref_ranges bounds chacha8rand_words)++-- pure xoshiro256++ ----------------------------------------------------------++-- n draws from a pure generator+draws :: (Pure.Gen -> Pure.Draw a) -> Int -> Pure.Gen -> [a]+draws f = go where+ go n g+ | n <= 0 = []+ | otherwise = case f g of+ Pure.Draw x ng -> x : go (n - 1) ng++pure_state :: Word64 -> Word64 -> Word64 -> Word64 -> IO Pure.Gen+pure_state s0 s1 s2 s3 = case Pure.from_state s0 s1 s2 s3 of+ Nothing -> H.assertFailure "invalid state"+ Just g -> pure g++pure_reference :: TestTree+pure_reference = H.testCase "reference output" $ do+ g <- pure_state 1 2 3 4+ H.assertEqual mempty [+ 41943041, 58720359, 3588806011781223, 3591011842654386+ , 9228616714210784205, 9973669472204895162, 14011001112246962877+ , 12406186145184390807, 15849039046786891736, 10450023813501588000+ ] (draws Pure.word64 10 g)++pure_jumps :: TestTree+pure_jumps = H.testCase "jump and long_jump" $ do+ g <- pure_state 1 2 3 4+ H.assertEqual "jump" [+ 0xec879073673df437, 0x20d212a39aca1eaa+ , 0xc19d712a27e40f57, 0x6ff0e08dc71026a1+ ] (draws Pure.word64 4 (Pure.jump g))+ H.assertEqual "long_jump" [+ 0xb5c4ea370b330bf5, 0x5173cc693c0fa533+ , 0x1dc5df0151f7b491, 0xe7b055cfeabc4661+ ] (draws Pure.word64 4 (Pure.long_jump g))++pure_new :: TestTree+pure_new = H.testCase "new (splitmix64 seeding)" $ do+ H.assertEqual "0" [ 0x53175d61490b23df, 0x61da6f3dc380d507+ , 0x5c0fdf91ec9a7bfc, 0x02eebf8c3bbe5e1a ]+ (draws Pure.word64 4 (Pure.new 0))+ H.assertEqual "42" [ 0xd0764d4f4476689f, 0x519e4174576f3791+ , 0xfbe07cfb0c24ed8c, 0xb37d9f600cd835b8 ]+ (draws Pure.word64 4 (Pure.new 42))++pure_zero_state :: TestTree+pure_zero_state = H.testCase "from_state rejects the zero state" $+ case Pure.from_state 0 0 0 0 of+ Nothing -> pure ()+ Just _ -> H.assertFailure "accepted zero state"++data Op = OpWord | OpDouble | OpRange Word64 | OpInt Int Int++data Out = OutWord Word64 | OutDouble Double | OutInt Int+ deriving (Eq, Show)++-- a mix of operations, including ranges with frequent rejection+ops :: [Op]+ops = concat . replicate 30 $ [+ OpWord, OpDouble, OpRange 6, OpRange (2 ^ (63 :: Int))+ , OpInt (-10) 10, OpInt minBound maxBound, OpRange maxBound+ ]++run_pure :: [Op] -> Pure.Gen -> [Out]+run_pure [] _ = []+run_pure (o : os) g = case o of+ OpWord -> case Pure.word64 g of+ Pure.Draw x ng -> OutWord x : run_pure os ng+ OpDouble -> case Pure.double g of+ Pure.Draw x ng -> OutDouble x : run_pure os ng+ OpRange n -> case Pure.word64_range 0 n g of+ Pure.Draw x ng -> OutWord x : run_pure os ng+ OpInt a b -> case Pure.int_range a b g of+ Pure.Draw x ng -> OutInt x : run_pure os ng++run_mutable :: [Op] -> PRNG.Gen RealWorld -> IO [Out]+run_mutable os g = traverse op os where+ op o = case o of+ OpWord -> fmap OutWord (PRNG.word64 g)+ OpDouble -> fmap OutDouble (PRNG.double g)+ OpRange n -> fmap OutWord (PRNG.word64_range 0 n g)+ OpInt a b -> fmap OutInt (PRNG.int_range a b g)++-- the pure and mutable generators produce the same output+pure_agrees :: TestTree+pure_agrees = H.testCase "agrees with Crypto.PRNG" $+ for_ [0, 42, maxBound] $ \seed -> do+ m <- PRNG.new seed+ out <- run_mutable ops m+ H.assertEqual (show seed) out (run_pure ops (Pure.new seed))
+ test/Vectors.hs view
@@ -0,0 +1,242 @@+{-# LANGUAGE OverloadedStrings #-}++-- ChaCha8Rand sample output, from etc/chacha8rand.md: the first 2976+-- bytes of output for the seed "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", as+-- hex, and as little-endian 64-bit words.++module Vectors (+ chacha8rand_seed+ , chacha8rand_hex+ , chacha8rand_words+ ) where++import qualified Data.ByteString as BS+import Data.Word (Word64)++chacha8rand_seed :: BS.ByteString+chacha8rand_seed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"++chacha8rand_hex :: BS.ByteString+chacha8rand_hex = 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"+ , "87045ee072df415f1d6be21a1e7ebd251da462d604409fbe46851883d458bf65"+ , "cc13db69ff7cb2d136bbc1723366a6014d7f727b57d78d57f63c086c068fc719"+ , "bb91c3f9d414e0dbb3ff131dddb2fb974f8deff90a1ec931ba432a4098fc4d09"+ , "52b737ea1bd69b06ca86d9e862d7725b85bc04598631ee723ec336cdc5fdf5d1"+ , "ad7c94a880499bba43ab49ac5ef0e8ece738aeab8411fe655214d3a5deb97c2d"+ , "e367e476944871cc8cc678a558a2034cfcd80fcb9edfef006d66e271d4ca2499"+ , "e965f7188366f887d8f5551b7cc54dcb594860865a8373d382e440558b5626e5"+ , "ec6f58080f04391fe6f89302f0f364b7a850bdf6a24394041a94d39786c8fe76"+ , "a2e7ba39d070fb3ea8a8ec681361f4e225241de0967a007cbfc5698e76e5ccbb"+ , "c3aa425c98b44f78743822aa91502bf7072ee6b13f333036deb8bbdeeb19738e"+ , "00fa59a9bc82392ab3a94b969f8bb9b25119b7ad1410e3f782cc3a70a3fcd0eb"+ , "9a41e62f2a4e65ec2c2ea5552d1326b3782950447fc5482216af2d342f0c7132"+ , "ec2bcb5a7bb41705370927ca8f717a4c41c5bcd0be4291d688ce52ffb8bc0ee4"+ , "d428f8c9dba2443e583f878f4f2f4cc75be499b78e64bf3df8860eee7524f233"+ , "657fd416eef9b41ee3442771b8d2f8407245b13cdab486b84df6d6bd6f328620"+ , "b982d87d90e53dccdf09e95e9ab4e8a2104c9623788efbdbd5f80def8960dd70"+ , "9fc9d9cd631614306503245c3205b804d612ac1403d88374f9f5a71ab91c272b"+ , "f0ddab625324e297ab9f2a2314f6845aa27f4bdafc2511f76772b2741da6a51c"+ , "45cbdb3a9b6acc38393e65dc85bbe1ddd49ff864fac8d0e988412bcd1efbc502"+ , "e55657ca7b13bdf255e11b125de2efad8e3a895d3c1ccd56b95bb6be37d3504c"+ , "67f55c6751518c911e6ab5cfff49a6abcd47226ab24ac720da083c85ac6b1671"+ , "5dfc84e5e2ef7bb032bf8d582aff45da5e09754d3cb098db4c5aa6aae15a2860"+ , "b84031266a683bf90e181cee529746deae9a1204dc32c2ce04ea3518aa6b91eb"+ , "ff8843b6c8219cd488488658962da872a866ac7eef483300eb09b255e6676f7f"+ , "251b947a0bfb2f53deee8d12e6ad40d923fe89afa1f224dfe05a1988493baa95"+ , "4abe944f4049a63d277e3f2c13ad2d69b89eaaca6ee7ae40245065091ea09412"+ , "f5e4a4dbab97f76d32701d4c02b62feafc89542992044e5f6ae022ea14299757"+ , "73d4aa33d137819af3f2cde76adde6a2476608184f64429f3ebd70c10133d016"+ , "6d6546a56f418c903e122ee23b5081e072ccc41691f07c0729f2b764d25cd2cb"+ , "314059ec68f4b23dd5ad9b4c730ec04661d87520c70aecd030760bf83ccb3730"+ , "c621273a7b3d4c574b8276006a9099ae702e538b41a575b1dd1d23ee51e2b3d8"+ , "6619ca5dc2ee33b4939aff5cdc300f5335d33cb5983df09fdf8c55765022c4af"+ , "2a408482a2d381ef280a111cf5db0b11f6e827d055b2e19a32836824aae0e37d"+ , "e27e06d2ecc383e4e63761278b3229f8ad2c5657adcc13a41fcb6a498b8e11e6"+ , "1fc05edaa6dc88825572c188dc7737a5da6e71d5e0f1008aa820a7b7476f8e61"+ , "41a892c6b007399ef3343f96ca428b97efd7a798cdb0e47552475f0b6ebd4dde"+ , "3f49343f15e45202f94e7303d8e7f050ee67d18ea3667723a039ee0140412441"+ , "21bb35e543f68dd0740ba8a9b575f53455f79772f83a342ccbf721d8996d8bcd"+ , "ae48fc5672fd76e385283534736eb0e1eb69c1866fb287fade71a9654660c136"+ , "808e9c23c247a1db240e7ffc698e206bc3602b6f5b399587f4074919e9beda05"+ , "02d95e2f147581b981dc1d02e201175e08ed5e75a2ab750895de519228838d77"+ , "31cb9e036ae4fb3bf9d7cbe4fc0447b23d1ec9a7e9ff8569bbda49f23db1efc8"+ , "f6c9f4b0647e03b1c3b7d697d19ff6550ca9681dd78925677ea7504f22b8bdbe"+ , "a7747300809f583f2a18545963f407d390fdd4100c85f5cf158e40b6df02dac6"+ , "85a4b1e2f1eeda9325a6ee8a2033d865fab5d33ea13fb1e28eb60f1338350567"+ , "a98f2198652f04c12e8a9b74caad5bee7de3da47f9a3226dafdbf457166d2cb6"+ , "0bc20497e67d006ed84138fc13b9f21a228e2e8e34470edc228b95cff1de1d9b"+ , "b8663023b0d62e63f2d8be11332dd0dde9564683e1cf47f17a5911d549aa9a39"+ , "0903ec796988146b97fb5a6bc34afc6481f07c7fe0782fb81b0e3d329a5c9210"+ , "f6633ce19ec751f4c776783180c12f7c227dcbeed92ba135901f6239a5545633"+ , "f081b55df3a332cccb69230ba84807c66b159185b0d34d7c9122e26d4bed1cac"+ , "f5de34f12dfa2ca3532aea8d91087162f44fcb8f60b155053ca3aa43ace73e14"+ , "18c24fcfe70ce9dab5f4bc8225fc684d715d1349184e0937d89fd4f3097e85f7"+ , "e78b7603c538750001e66b2fba48f6ed3e5172dd647634aa863bf26e3c8963be"+ , "97af050671850b1356abf61e6b5c76dd6bdc979a629a24f3e5b8fa20904f112a"+ , "08adc6cf27e0695a50e0a5f136cb4c3ca5f03468597d9e2e89e7fc5868be3024"+ , "97e566242f860be9eca959f184285e8957cb2f90a48fab2650fae1545cffefa6"+ , "55821a81e5c43a33118649025f518da5f46e5cb2dc095afe18585fab88898903"+ , "17c6f62a24f69f2823ea81d39fd59d3d51ae8a8a3dd9d7526f781f513d126ac7"+ , "6cc400afed0189f680de90b57108638c91e09189309c2005778178b4999f801f"+ , "d89fc1b62e0c171158ba6290773c4344454c87f11ab5acc0a19f808442132e9f"+ , "fa19c615bd23b5edc023cc3ed57fd9024c376244a305afac1fa1ff4bd3c6d9dd"+ ]++chacha8rand_words :: [Word64]+chacha8rand_words = [+ 0xb773b6063d4616a5, 0x1160af22a66abc3c, 0x8c2599d9418d287c+ , 0x7ee07e037edc5cd6, 0xcfaa9ee02d1c16ad, 0x0e090eef8febea79+ , 0x3c82d271128b5b3e, 0x9c5addc11252a34f, 0xdf79bb617d6ceea6+ , 0x36d553591f9d736a, 0xeef0d14e181ee01f, 0x089bfc760ae58436+ , 0xd9e52b59cc2ad268, 0xeb2fb4444b1b8aba, 0x4f95c8a692c46661+ , 0xc3c6323217cae62c, 0x91ebb4367f4e2e7e, 0x784cf2c6a0ec9bc6+ , 0x5c34ec5c34eabe20, 0x4f0a8f515570daa8, 0xfc35dcb4113d6bf2+ , 0x5b0da44c645554bc, 0x6d963da3db21d9e1, 0xeeaefc3150e500f3+ , 0x2d37923dda3750a5, 0x380d7a626d4bc8b0, 0xeeaf68ede3d7ee49+ , 0xf4356695883b717c, 0x846a9021392495a4, 0x8e8510549630a61b+ , 0x18dc02545dbae493, 0x0f8f9ff0a65a3d43, 0xccf065f7190ff080+ , 0xfd76d1aa39673330, 0x95d232936cba6433, 0x6c7456d1070cbd17+ , 0x462acfdaff8c6562, 0x5bafab866d34fc6a, 0x0c862f78030a2988+ , 0xd39a83e407c3163d, 0xc00a2b7b45f22ebf, 0x564307c62466b1a9+ , 0x257e0424b0c072d4, 0x6fb55e99496c28fe, 0xae9873a88f5cd4e0+ , 0x4657362ac60d3773, 0x1c83f91ecdf23e8e, 0x6fdc0792c15387c0+ , 0x36dad2a30dfd2b5c, 0xa4b593290595bdb7, 0x4de18934e4cc02c5+ , 0xcdc0d604f015e3a7, 0xfba0dbf69ad80321, 0x60e8bea3d139de87+ , 0xd18a4d851ef48756, 0x6366447c2215f34a, 0x05682e97d3d007ee+ , 0x4c0e8978c6d54ab2, 0xcf1e9f6a6712edc2, 0x061439414c80cfd3+ , 0xd1a8b6e2745c0ead, 0x31a7918d45c410e8, 0xabcc61ad90216eec+ , 0x4040d92d2032a71a, 0x3cd2f66ffb40cd68, 0xdcd051c07295857a+ , 0xeab55cbcd9ab527e, 0x18471dce781bdaac, 0xf7f08cd144dc7252+ , 0x5804e0b13d7f40d1, 0x5cb1a446e4b2d35b, 0xe6d4a728d2138a06+ , 0x05223e40ca60dad8, 0x2d61ec3206ac6a68, 0xab692356874c17b8+ , 0xc30954417676de1c, 0x4f1ace3732225624, 0xfba9510813988338+ , 0x997f200f52752e11, 0x1116aaafe86221fa, 0x07ce3b5cb2a13519+ , 0x2956bc72bc458314, 0x4188b7926140eb78, 0x56ca6dbfd4adea4d+ , 0x7fe3c22349340ce5, 0x35c08f9c37675f8a, 0x11e1c7fbef5ed521+ , 0x98adc8464ec1bc75, 0xd163b2c73d1203f8, 0x8c761ee043a2f3f3+ , 0x24b99d6accecd7b7, 0x793e31aa112f0370, 0x8e87dc2a19285139+ , 0x4247ae04f7096e25, 0x514f3122926fe20f, 0xdc6fb3f045d2a7e9+ , 0x15cb30cecdd18eba, 0xcbc7fdecf6900274, 0x3fb5c696dc8ba021+ , 0xd1664417c8d274e6, 0x05f7e445ea457278, 0xf920bbca1b9db657+ , 0x0c1950b4da22cb99, 0xf875baf1af09e292, 0xbed3d7b84250f838+ , 0xf198e8080fd74160, 0xc9eda51d9b7ea703, 0xf709ef55439bf8f6+ , 0xd20c74feebf116fc, 0x305668eb146d7546, 0x829af3ec10d89787+ , 0x15b8f9697b551dbc, 0xfc823c6c8e64b8c9, 0x345585e8183b40bc+ , 0x674b4171d6581368, 0x1234d81cd670e9f7, 0x0e505210d8a55e19+ , 0xe8258d69eeeca0dc, 0x05d4c452e8baf67e, 0xe8dbe30116a45599+ , 0x1cf08ce1b1176f00, 0xccf7d0a4b81ecb49, 0x303fea136b2c430e+ , 0x861d6c139c06c871, 0x5f41df72e05e0487, 0x25bd7e1e1ae26b1d+ , 0xbe9f4004d662a41d, 0x65bf58d483188546, 0xd1b27cff69db13cc+ , 0x01a6663372c1bb36, 0x578dd7577b727f4d, 0x19c78f066c083cf6+ , 0xdbe014d4f9c391bb, 0x97fbb2dd1d13ffb3, 0x31c91e0af9ef8d4f+ , 0x094dfc98402a43ba, 0x069bd61bea37b752, 0x5b72d762e8d986ca+ , 0x72ee31865904bc85, 0xd1f5fdc5cd36c33e, 0xba9b4980a8947cad+ , 0xece8f05eac49ab43, 0x65fe1184abae38e7, 0x2d7cb9dea5d31452+ , 0xcc71489476e467e3, 0x4c03a258a578c68c, 0x00efdf9ecb0fd8fc+ , 0x9924cad471e2666d, 0x87f8668318f765e9, 0xcb4dc57c1b55f5d8+ , 0xd373835a86604859, 0xe526568b5540e482, 0x1f39040f08586fec+ , 0xb764f3f00293f8e6, 0x049443a2f6bd50a8, 0x76fec88697d3941a+ , 0x3efb70d039bae7a2, 0xe2f4611368eca8a8, 0x7c007a96e01d2425+ , 0xbbcce5768e69c5bf, 0x784fb4985c42aac3, 0xf72b5091aa223874+ , 0x3630333fb1e62e07, 0x8e7319ebdebbb8de, 0x2a3982bca959fa00+ , 0xb2b98b9f964ba9b3, 0xf7e31014adb71951, 0xebd0fca3703acc82+ , 0xec654e2a2fe6419a, 0xb326132d55a52e2c, 0x2248c57f44502978+ , 0x32710c2f342daf16, 0x0517b47b5acb2bec, 0x4c7a718fca270937+ , 0xd69142bed0bcc541, 0xe40ebcb8ff52ce88, 0x3e44a2dbc9f828d4+ , 0xc74c2f4f8f873f58, 0x3dbf648eb799e45b, 0x33f22475ee0e86f8+ , 0x1eb4f9ee16d47f65, 0x40f8d2b8712744e3, 0xb886b4da3cb14572+ , 0x2086326fbdd6f64d, 0xcc3de5907dd882b9, 0xa2e8b49a5ee909df+ , 0xdbfb8e7823964c10, 0x70dd6089ef0df8d5, 0x30141663cdd9c99f+ , 0x04b805325c240365, 0x7483d80314ac12d6, 0x2b271cb91aa7f5f9+ , 0x97e2245362abddf0, 0x5a84f614232a9fab, 0xf71125fcda4b7fa2+ , 0x1ca5a61d74b27267, 0x38cc6a9b3adbcb45, 0xdde1bb85dc653e39+ , 0xe9d0c8fa64f89fd4, 0x02c5fb1ecd2b4188, 0xf2bd137bca5756e5+ , 0xadefe25d121be155, 0x56cd1c3c5d893a8e, 0x4c50d337beb65bb9+ , 0x918c5151675cf567, 0xaba649ffcfb56a1e, 0x20c74ab26a2247cd+ , 0x71166bac853c08da, 0xb07befe2e584fc5d, 0xda45ff2a588dbf32+ , 0xdb98b03c4d75095e, 0x60285ae1aaa65a4c, 0xf93b686a263140b8+ , 0xde469752ee1c180e, 0xcec232dc04129aae, 0xeb916baa1835ea04+ , 0xd49c21c8b64388ff, 0x72a82d9658864888, 0x003348ef7eac66a8+ , 0x7f6f67e655b209eb, 0x532ffb0b7a941b25, 0xd940ade6128deede+ , 0xdf24f2a1af89fe23, 0x95aa3b4988195ae0, 0x3da649404f94be4a+ , 0x692dad132c3f7e27, 0x40aee76ecaaa9eb8, 0x1294a01e09655024+ , 0x6df797abdba4e4f5, 0xea2fb6024c1d7032, 0x5f4e0492295489fc+ , 0x57972914ea22e06a, 0x9a8137d133aad473, 0xa2e6dd6ae7cdf2f3+ , 0x9f42644f18086647, 0x16d03301c170bd3e, 0x908c416fa546656d+ , 0xe081503be22e123e, 0x077cf09116c4cc72, 0xcbd25cd264b7f229+ , 0x3db2f468ec594031, 0x46c00e734c9badd5, 0xd0ec0ac72075d861+ , 0x3037cb3cf80b7630, 0x574c3d7b3a2721c6, 0xae99906a0076824b+ , 0xb175a5418b532e70, 0xd8b3e251ee231ddd, 0xb433eec25dca1966+ , 0x530f30dc5cff9a93, 0x9ff03d98b53cd335, 0xafc4225076558cdf+ , 0xef81d3a28284402a, 0x110bdbf51c110a28, 0x9ae1b255d027e8f6+ , 0x7de3e0aa24688332, 0xe483c3ecd2067ee2, 0xf829328b276137e6+ , 0xa413ccad57562cad, 0xe6118e8b496acb1f, 0x8288dca6da5ec01f+ , 0xa53777dc88c17255, 0x8a00f1e0d5716eda, 0x618e6f47b7a720a8+ , 0x9e3907b0c692a841, 0x978b42ca963f34f3, 0x75e4b0cd98a7d7ef+ , 0xde4dbd6e0b5f4752, 0x0252e4153f34493f, 0x50f0e7d803734ef9+ , 0x237766a38ed167ee, 0x4124414001ee39a0, 0xd08df643e535bb21+ , 0x34f575b5a9a80b74, 0x2c343af87297f755, 0xcd8b6d99d821f7cb+ , 0xe376fd7256fc48ae, 0xe1b06e7334352885, 0xfa87b26f86c169eb+ , 0x36c1604665a971de, 0xdba147c2239c8e80, 0x6b208e69fc7f0e24+ , 0x8795395b6f2b60c3, 0x05dabee9194907f4, 0xb98175142f5ed902+ , 0x5e1701e2021ddc81, 0x0875aba2755eed08, 0x778d83289251de95+ , 0x3bfbe46a039ecb31, 0xb24704fce4cbd7f9, 0x6985ffe9a7c91e3d+ , 0xc8efb13df249dabb, 0xb1037e64b0f4c9f6, 0x55f69fd197d6b7c3+ , 0x672589d71d68a90c, 0xbebdb8224f50a77e, 0x3f589f80007374a7+ , 0xd307f4635954182a, 0xcff5850c10d4fd90, 0xc6da02dfb6408e15+ , 0x93daeef1e2b1a485, 0x65d833208aeea625, 0xe2b13fa13ed3b5fa+ , 0x67053538130fb68e, 0xc1042f6598218fa9, 0xee5badca749b8a2e+ , 0x6d22a3f947dae37d, 0xb62c6d1657f4dbaf, 0x6e007de69704c20b+ , 0x1af2b913fc3841d8, 0xdc0e47348e2e8e22, 0x9b1ddef1cf958b22+ , 0x632ed6b0233066b8, 0xddd02d3311bed8f2, 0xf147cfe1834656e9+ , 0x399aaa49d511597a, 0x6b14886979ec0309, 0x64fc4ac36b5afb97+ , 0xb82f78e07f7cf081, 0x10925c9a323d0e1b, 0xf451c79ee13c63f6+ , 0x7c2fc180317876c7, 0x35a12bd9eecb7d22, 0x335654a539621f90+ , 0xcc32a3f35db581f0, 0xc60748a80b2369cb, 0x7c4dd3b08591156b+ , 0xac1ced4b6de22291, 0xa32cfa2df134def5, 0x627108918dea2a53+ , 0x0555b1608fcb4ff4, 0x143ee7ac43aaa33c, 0xdae90ce7cf4fc218+ , 0x4d68fc2582bcf4b5, 0x37094e1849135d71, 0xf7857e09f3d49fd8+ , 0x007538c503768be7, 0xedf648ba2f6be601, 0xaa347664dd72513e+ , 0xbe63893c6ef23b86, 0x130b85710605af97, 0xdd765c6b1ef6ab56+ , 0xf3249a629a97dc6b, 0x2a114f9020fab8e5, 0x5a69e027cfc6ad08+ , 0x3c4ccb36f1a5e050, 0x2e9e7d596834f0a5, 0x2430be6858fce789+ , 0xe90b862f2466e597, 0x895e2884f159a9ec, 0x26ab8fa4902fcb57+ , 0xa6efff5c54e1fa50, 0x333ac4e5811a8255, 0xa58d515f02498611+ , 0xfe5a09dcb25c6ef4, 0x03898988ab5f5818, 0x289ff6242af6c617+ , 0x3d9dd59fd381ea23, 0x52d7d93d8a8aae51, 0xc76a123d511f786f+ , 0xf68901edaf00c46c, 0x8c630871b590de80, 0x05209c308991e091+ , 0x1f809f99b4788177, 0x11170c2eb6c19fd8, 0x44433c779062ba58+ , 0xc0acb51af1874c45, 0x9f2e134284809fa1, 0xedb523bd15c619fa+ , 0x02d97fd53ecc23c0, 0xacaf05a34462374c, 0xddd9c6d34bffa11f+ ]