packages feed

ppad-random-0.1.0: lib/Crypto/CSPRNG.hs

{-# 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 #-}