packages feed

crypto-rng 0.3.0.1 → 0.3.0.2

raw patch · 9 files changed

+289/−114 lines, 9 filesdep +containersdep +crypto-rngdep +tastydep ~basedep ~bytestringdep ~primitivePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: containers, crypto-rng, tasty, tasty-hunit

Dependency ranges changed: base, bytestring, primitive, random

API changes (from Hackage documentation)

- Crypto.RNG: data CryptoRNGT m a
+ Crypto.RNG: data CryptoRNGT (m :: Type -> Type) a
- Crypto.RNG: newCryptoRNGStateSized :: MonadIO m => Int -> m CryptoRNGState
+ Crypto.RNG: newCryptoRNGStateSized :: (HasCallStack, MonadIO m) => Int -> m CryptoRNGState
- Crypto.RNG.Class: class Monad m => CryptoRNG m
+ Crypto.RNG.Class: class Monad m => CryptoRNG (m :: Type -> Type)
- Crypto.RNG.Unsafe: data RNGT m a
+ Crypto.RNG.Unsafe: data RNGT (m :: Type -> Type) a
- Crypto.RNG.Utils: randomString :: CryptoRNG m => Int -> [Char] -> m String
+ Crypto.RNG.Utils: randomString :: (HasCallStack, CryptoRNG m) => Int -> [Char] -> m String

Files

+ CHANGELOG.md view
@@ -0,0 +1,41 @@+# crypto-rng-0.3.0.2 (2026-09-17)+* Fix a bug that made the source of random bytes repeat bytes it already+  returned.+* If the list of allowed chars is empty, `randomString` raises an error. It+  used to return a string of garbage that crashed the program on use.+* A value from `random` no longer keeps the internal buffer of random bytes+  alive until the value is evaluated.+* Add support for `random` >= 1.3.+* Drop support for GHC 8.8.++# crypto-rng-0.3.0.1 (2022-02-24)+* Improve performance with multiple capabilities.++# crypto-rng-0.3.0.0 (2022-02-21)+* Use the entropy package instead of DRBG.++# crypto-rng-0.2.0.1 (2022-02-16)+* Better selection strategy for picking generators from the pool.++# crypto-rng-0.2.0.0 (2022-02-16)+* Drop support for GHC < 8.8+* Fix a space leak in randomBytesIO.+* Use a buffered generator.+* Remove modulo bias from randomRIO.+* Improve performance of randomString.+* Add support for a pool of generators for less contention.++# crypto-rng-0.1.2.0 (2020-05-05)+* GHC-8.8 support (MonadFail) and ghc 8.10.1 support.++# crypto-rng-0.1.1.0 (2019-10-08)+* Added a 'MonadError' instance for 'CryptoRNGT'.++# crypto-rng-0.1.0.2 (2018-03-14)+* Dropped support for GHC 7.8 and 7.10.++# crypto-rng-0.1.0.1 (2017-01-18)+* Removed a redundant constraint that led to build failures with GHC 8.0.2.++# crypto-rng-0.1.0.0 (2016-12-06)+* First version. Released on an unsuspecting world.
− ChangeLog.md
@@ -1,42 +0,0 @@-# Revision history for crypto-rng--## 0.3.0.1  -- 2022-02-24--* Improve performance with multiple capabilities.--## 0.3.0.0  -- 2022-02-21--* Use the entropy package instead of DRBG.--## 0.2.0.1  -- 2022-02-16--* Better selection strategy for picking generators from the pool.--## 0.2.0.0  -- 2022-02-16--* Drop support for GHC < 8.8-* Fix a space leak in randomBytesIO.-* Use a buffered generator.-* Remove modulo bias from randomRIO.-* Improve performance of randomString.-* Add support for a pool of generators for less contention.--## 0.1.2.0  -- 2020-05-05--* GHC-8.8 support (MonadFail) and ghc 8.10.1 support.--## 0.1.1.0  -- 2019-10-08--* Added a 'MonadError' instance for 'CryptoRNGT'.--## 0.1.0.2  -- 2018-03-14--* Dropped support for GHC 7.8 and 7.10.--## 0.1.0.1  -- 2017-01-18--* Removed a redundant constraint that led to build failures with GHC 8.0.2.--## 0.1.0.0  -- 2016-12-06--* First version. Released on an unsuspecting world.
+ README.md view
@@ -0,0 +1,9 @@+# crypto-rng++[![Build Status](https://github.com/scrive/crypto-rng/actions/workflows/haskell-ci.yml/badge.svg?branch=master)](https://github.com/scrive/crypto-rng/actions?query=branch%3Amaster)+[![Hackage](https://img.shields.io/hackage/v/crypto-rng.svg)](https://hackage.haskell.org/package/crypto-rng)+[![Stackage LTS](https://www.stackage.org/package/crypto-rng/badge/lts)](https://www.stackage.org/lts/package/crypto-rng)+[![Stackage Nightly](https://www.stackage.org/package/crypto-rng/badge/nightly)](https://www.stackage.org/nightly/package/crypto-rng)++A convenient interface to the cryptographically secure random number generator+backed by the [entropy](http://hackage.haskell.org/package/entropy) package.
crypto-rng.cabal view
@@ -1,45 +1,75 @@+cabal-version:       3.0 name:                crypto-rng-version:             0.3.0.1-synopsis:            Cryptographic random number generator.+version:             0.3.0.2+synopsis:            Cryptographically secure random number generator.  description:         Convenient wrapper for the source of random bytes                      provided by the @entropy@ package.  homepage:            https://github.com/scrive/crypto-rng-license:             BSD3+license:             BSD-3-Clause license-file:        LICENSE author:              Scrive AB-maintainer:          Mikhail Glushenkov <mikhail.glushenkov@scrive.com>,-                     Jonathan Jouty <jonathan@scrive.com>+maintainer:          Andrzej Rybczak <andrzej.rybczak@scrive.com> copyright:           Scrive AB category:            Crypto build-type:          Simple-tested-with:         GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.1-extra-source-files:  ChangeLog.md-cabal-version:       >=1.10+tested-with:         GHC == { 8.10.7, 9.0.2, 9.2.8, 9.4.8, 9.6.7, 9.8.4, 9.10.3, 9.12.4, 9.14.1 }+extra-doc-files:     CHANGELOG.md+                   , README.md  source-repository head   type:     git   location: https://github.com/scrive/crypto-rng.git +common language+  ghc-options:         -Wall -Wcompat -Wredundant-constraints+                       -Werror=prepositive-qualified-module++  default-language:    Haskell2010++  default-extensions:  FlexibleInstances+                       GeneralizedNewtypeDeriving+                       ImportQualifiedPost+                       MultiParamTypeClasses+                       TypeApplications+                       UndecidableInstances+ library-  ghc-options:        -Wall -Wcompat+  import:              language    exposed-modules:     Crypto.RNG                        Crypto.RNG.Class                        Crypto.RNG.Utils                        Crypto.RNG.Unsafe -  build-depends:       base              >= 4.13    && < 5+  build-depends:       base              >= 4.14    && <5                      , bytestring        >= 0.10.8                      , entropy           >= 0.4                      , exceptions        >= 0.8.3                      , monad-control     >= 1.0.1                      , mtl               >= 2.2-                     , primitive         >= 0.7-                     , random            >= 1.2     && <1.3+                     , primitive         >= 0.7.1+                     , random            >= 1.2     && <1.4                      , transformers-base >= 0.4.4    hs-source-dirs:      src -  default-language:    Haskell2010+test-suite test+  import:              language++  type:                exitcode-stdio-1.0+  main-is:             Main.hs++  ghc-options:         -threaded -rtsopts++  build-depends:       base+                     , bytestring+                     , containers  >= 0.6+                     , crypto-rng+                     , primitive   >= 0.7.1+                     , random      >= 1.2+                     , tasty       >= 1.4+                     , tasty-hunit >= 0.10++  hs-source-dirs:      test
src/Crypto/RNG.hs view
@@ -1,14 +1,13 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CPP #-} -- | Support for generation of cryptographically secure random numbers. ----- This is a convenience layer on top of "System.Entropy", which allows you to--- pull random values by means of the class 'CryptoRNG', while keeping the state--- of the random number generator (RNG) inside a monad. The state is protected--- by an MVar, which means that concurrent generation of random values from--- several threads works straight out of the box.+-- This is a convenience layer on top of "System.Entropy". You pull random+-- values with the class 'CryptoRNG'. A monad keeps the state of the random+-- number generator (RNG).+--+-- The state holds one buffer per capability, and an MVar protects each+-- buffer. A thread uses the buffer of the capability it runs on, so threads+-- on different capabilities do not contend. module Crypto.RNG   ( -- * CryptoRNG class     module Crypto.RNG.Class@@ -34,29 +33,47 @@ import Control.Monad.Reader import Control.Monad.Trans.Control import Data.Bits-import Data.ByteString (ByteString)+import Data.ByteString qualified as BS import Data.Primitive.SmallArray+import GHC.Stack import System.Entropy-import qualified Data.ByteString as BS-import qualified Data.ByteString.Short as SBS-import qualified System.Random.Stateful as R+import System.Random.Stateful qualified as R  import Crypto.RNG.Class +#if MIN_VERSION_random(1,3,0)+import Data.ByteString.Unsafe qualified as BSU+import Data.Primitive.ByteArray+#else+import Data.ByteString.Short qualified as SBS+#endif+ -- | The random number generator state. data CryptoRNGState = CryptoRNGState !Int !(SmallArray (MVar Buffer))  -- | A buffer of random bytes for immediate consumption. newtype Buffer = Buffer { bytes :: BS.ByteString } +-- The results are strict, because a lazy result would hold the slice of the+-- buffer and thus the whole buffer until it is evaluated. instance R.StatefulGen CryptoRNGState IO where-  uniformWord8  st = mkWord <$> randomBytesIO 1 st-  uniformWord16 st = mkWord <$> randomBytesIO 2 st-  uniformWord32 st = mkWord <$> randomBytesIO 4 st-  uniformWord64 st = mkWord <$> randomBytesIO 8 st-  uniformShortByteString n st = SBS.toShort <$> randomBytesIO n st+  uniformWord8  st = mkWord <$!> randomBytesIO 1 st+  uniformWord16 st = mkWord <$!> randomBytesIO 2 st+  uniformWord32 st = mkWord <$!> randomBytesIO 4 st+  uniformWord64 st = mkWord <$!> randomBytesIO 8 st+#if MIN_VERSION_random(1,3,0)+  uniformByteArrayM isPinned n st = do+    bs <- randomBytesIO n st+    let len = BS.length bs+    mba <- if isPinned then newPinnedByteArray len else newByteArray len+    BSU.unsafeUseAsCStringLen bs $ \(ptr, _) ->+      copyPtrToMutableByteArray mba 0 ptr len+    unsafeFreezeByteArray mba+#else+  uniformShortByteString n st = SBS.toShort <$!> randomBytesIO n st+#endif -mkWord :: (Bits a, Integral a) => ByteString -> a+mkWord :: (Bits a, Integral a) => BS.ByteString -> a mkWord bs = BS.foldl' (\acc w -> shiftL acc 8 .|. fromIntegral w) 0 bs  ----------------------------------------@@ -73,7 +90,7 @@ -- -- One buffer per capability is created. newCryptoRNGStateSized-  :: MonadIO m+  :: (HasCallStack, MonadIO m)   => Int -- ^ Buffer size.   -> m CryptoRNGState newCryptoRNGStateSized maxBufSize = liftIO $ do@@ -84,36 +101,20 @@   pure $ CryptoRNGState maxBufSize (smallArrayFromListN n bufs)  -- | Generate a number of cryptographically secure random bytes.-randomBytesIO :: Int -> CryptoRNGState -> IO ByteString+randomBytesIO :: Int -> CryptoRNGState -> IO BS.ByteString randomBytesIO n (CryptoRNGState maxBufSize bufs) = do   (cid, _) <- threadCapability =<< myThreadId   let mbuf = bufs `indexSmallArray` (cid `rem` sizeofSmallArray bufs)   modifyMVar mbuf $ \buf -> do-    -- Unroll the first step of 'generateBytes' as the vast majority of time-    -- it's enough to get the full amount of requested bytes.     let (r, newBytes) = BS.splitAt n (bytes buf)-    let k = n - BS.length r+        k = n - BS.length r     if k <= 0-      then newBytes `seq` pure (Buffer newBytes, r)+      then pure (Buffer newBytes, r)       else do-        (rs, newBuf) <- generateBytes maxBufSize buf k [r]-        pure (newBuf, BS.concat rs)--generateBytes-  :: Int-  -> Buffer-  -> Int-  -> [BS.ByteString]-  -> IO ([BS.ByteString], Buffer)-generateBytes maxBufSize buf n acc = do-  (r, newBytes) <- BS.splitAt n <$> if BS.null (bytes buf)-                                    then getEntropy maxBufSize-                                    else pure (bytes buf)-  let newBuf = Buffer newBytes-      k = n - BS.length r-  newBuf `seq` if k <= 0-    then pure (r : acc, newBuf)-    else generateBytes maxBufSize newBuf k (r : acc)+        -- The buffer is drained at this point. One call to the entropy source+        -- covers the missing bytes and the new buffer, whichever is larger.+        (rest, newerBytes) <- BS.splitAt k <$> getEntropy (max maxBufSize k)+        pure (Buffer newerBytes, r <> rest)  ---------------------------------------- 
src/Crypto/RNG/Class.hs view
@@ -1,22 +1,22 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE UndecidableInstances #-}-module Crypto.RNG.Class where+module Crypto.RNG.Class+  ( CryptoRNG(..)+  ) where  import Control.Monad.Trans-import Data.ByteString (ByteString)-import System.Random (Uniform, UniformRange)+import Data.ByteString qualified as BS+import System.Random qualified as R  -- | Monads carrying around the RNG state. class Monad m => CryptoRNG m where   -- | Generate a given number of cryptographically secure random bytes.-  randomBytes :: Int -> m ByteString+  randomBytes :: Int -> m BS.ByteString    -- | Generate a cryptographically secure value uniformly distributed over all   -- possible values of that type.-  random :: Uniform a => m a+  random :: R.Uniform a => m a    -- | Generate a cryptographically secure value in a given, closed range.-  randomR :: UniformRange a => (a, a) -> m a+  randomR :: R.UniformRange a => (a, a) -> m a  -- | Generic, overlapping instance. instance {-# OVERLAPPABLE #-}
src/Crypto/RNG/Unsafe.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CPP #-} -- | Support for generation of __non cryptographically secure__ random numbers -- for testing purposes. module Crypto.RNG.Unsafe@@ -25,7 +24,8 @@ import Control.Monad.Except import Control.Monad.Reader import Control.Monad.Trans.Control-import qualified System.Random as R+import Data.ByteString qualified as BS+import System.Random qualified as R  import Crypto.RNG.Class @@ -57,7 +57,7 @@ withRNGState = RNGT . ReaderT  instance MonadIO m => CryptoRNG (RNGT m) where-  randomBytes n  = RNGT ask >>= (`withRNG` \g -> R.genByteString n g)+  randomBytes n  = RNGT ask >>= (`withRNG` \g -> uniformByteString n g)   random         = RNGT ask >>= (`withRNG` \g -> R.uniform g)   randomR bounds = RNGT ask >>= (`withRNG` \g -> R.uniformR bounds g) @@ -65,3 +65,10 @@ withRNG (RNGState rng) f = liftIO . modifyMVar rng $ \g -> do   (a, newG) <- pure $ f g   newG `seq` pure (newG, a)++uniformByteString :: R.RandomGen g => Int -> g -> (BS.ByteString, g)+#if MIN_VERSION_random(1,3,0)+uniformByteString = R.uniformByteString+#else+uniformByteString = R.genByteString+#endif
src/Crypto/RNG/Utils.hs view
@@ -1,13 +1,24 @@-module Crypto.RNG.Utils where+module Crypto.RNG.Utils+  ( randomString+  ) where  import Control.Monad import Data.Primitive.SmallArray+import GHC.Stack  import Crypto.RNG  -- | Generate random string of specified length that contains allowed chars.-randomString :: CryptoRNG m => Int -> [Char] -> m String-randomString n allowedList = map (indexSmallArray allowed)-  <$> replicateM n (randomR (0, sizeofSmallArray allowed - 1))+--+-- The list of allowed chars must not be empty.+randomString :: (HasCallStack, CryptoRNG m) => Int -> [Char] -> m String+randomString n allowedList+  | size == 0 = error "List of allowed chars must not be empty"+  | otherwise = map (indexSmallArray allowed)+      <$> replicateM n (randomR (0, size - 1))   where+    allowed :: SmallArray Char     allowed = smallArrayFromList allowedList++    size :: Int+    size = sizeofSmallArray allowed
+ test/Main.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE CPP #-}+module Main (main) where++import Control.Exception+import Control.Monad+import Data.ByteString qualified as BS+import Data.Set qualified as S+import Test.Tasty+import Test.Tasty.HUnit++import Crypto.RNG+import Crypto.RNG.Utils++#if MIN_VERSION_random(1,3,0)+import Data.ByteString.Short qualified as SBS+import Data.Primitive.ByteArray+import System.Random.Stateful qualified as R+#endif++main :: IO ()+main = defaultMain $ testGroup "crypto-rng"+  [ testGroup "randomBytesIO" $ map bufferRefill configurations+  , testGroup "randomString"+    [ testCase "draws from the allowed chars" $ do+        rng <- newCryptoRNGState+        s <- runCryptoRNGT rng $ randomString 1000 alphabet+        assertEqual "length" 1000 (length s)+        assertBool "every char is allowed" $ all (`elem` alphabet) s+        assertBool "the whole alphabet shows up" $ all (`elem` s) alphabet+    , testCase "rejects an empty list of allowed chars" $ do+        rng <- newCryptoRNGState+        r <- try @ErrorCall $+          evaluate . length =<< runCryptoRNGT rng (randomString 8 "")+        case r of+          Left _ -> pure ()+          Right len -> assertFailure $ "returned a string of length " ++ show len+    ]+#if MIN_VERSION_random(1,3,0)+  , testGroup "uniformByteArrayM" [byteArrays]+#endif+  ]+  where+    alphabet :: [Char]+    alphabet = ['a' .. 'z'] ++ ['0' .. '9']++#if MIN_VERSION_random(1,3,0)+-- | The conversion from the generated bytes used to prepend a serialized+-- length, so the results were longer than requested and started with bytes+-- that were not random.+byteArrays :: TestTree+byteArrays = testCase "results have the requested length" $ do+  rng <- newCryptoRNGState+  forM_ [0, 1, 8, 16, 100, 1000] $ \n -> do+    forM_ [False, True] $ \isPinned -> do+      ba <- R.uniformByteArrayM isPinned n rng+      assertEqual ("byte array of " ++ show n) n (sizeofByteArray ba)+      when (isPinned && n > 0) $ do+        assertBool ("byte array of " ++ show n ++ " is pinned") (isByteArrayPinned ba)+    sbs <- R.uniformShortByteStringM n rng+    assertEqual ("short byte string of " ++ show n) n (SBS.length sbs)+#endif++-- | Buffer size paired with the request sizes to cycle through.+--+-- A request that is larger than the bytes left in the buffer is what triggers a+-- refill, so in each configuration the request sizes do not divide the buffer+-- size evenly.+configurations :: [(Int, [Int])]+configurations =+  [ (16, [10])+  , (16, [1, 7, 13, 40])+  , (32, [100])+  , (64, [20])+  , (32 * 1024, [100])+  , (1024, [3000, 10])+  , (1, [1, 2, 3])+  ]++-- | A refill used to hand out the bytes of the drained buffer a second time, so+-- a returned chunk repeated its own prefix and the repeat showed up again in+-- the following chunk.+bufferRefill :: (Int, [Int]) -> TestTree+bufferRefill (bufSize, sizes) = testCase name $ do+  rng <- newCryptoRNGStateSized bufSize+  chunks <- forM requestSizes $ \n -> do+    chunk <- randomBytesIO n rng+    assertEqual ("length of a " ++ show n ++ " byte request") n (BS.length chunk)+    assertBool ("a " ++ show n ++ " byte request repeats its own prefix") $+      not (repeatsPrefix 4 chunk)+    pure chunk+  let ws = windows 8 $ BS.concat chunks+  assertEqual "repeated windows" 0 (length ws - S.size (S.fromList ws))+  where+    name :: String+    name = "buffer of " ++ show bufSize ++ " bytes, requests of " ++ show sizes++    -- Enough requests to drain and refill the buffer several times.+    requestSizes :: [Int]+    requestSizes = takeUntilTotal (max 30000 (4 * bufSize)) (cycle sizes)++    takeUntilTotal :: Int -> [Int] -> [Int]+    takeUntilTotal _ [] = []+    takeUntilTotal remaining (n : ns)+      | remaining <= 0 = []+      | otherwise = n : takeUntilTotal (remaining - n) ns++    -- A shift by less than minLen bytes is left out, because a short match+    -- happens by chance often enough.+    repeatsPrefix :: Int -> BS.ByteString -> Bool+    repeatsPrefix minLen chunk = any matches [1 .. BS.length chunk - minLen]+      where+        matches :: Int -> Bool+        matches p = BS.drop p chunk == BS.take (BS.length chunk - p) chunk++    windows :: Int -> BS.ByteString -> [BS.ByteString]+    windows k bs+      | BS.length bs < k = []+      | otherwise = BS.take k bs : windows k (BS.drop 1 bs)