@@ -40,6 +40,10 @@ from math import ceil
4040
4141from cpython cimport PyMem_Malloc, PyMem_Free
4242from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release
43+ from cpython.bytes cimport PyBytes_FromStringAndSize, PyBytes_AsString
44+ from libc.stdlib cimport malloc, free
45+ from libc.stdint cimport uint8_t, uint32_t, uint64_t
46+ from libc.string cimport memset, memcpy
4347
4448API_VERSION = ' 1.3_01'
4549
@@ -714,3 +718,161 @@ def blake2b_256(key, data):
714718
715719def blake2b_128 (data ):
716720 return hashlib.blake2b(data, digest_size = 16 ).digest()
721+
722+
723+ cdef class CSPRNG:
724+ """
725+ Cryptographically Secure Pseudo-Random Number Generator based on AES-CTR mode.
726+
727+ This class provides methods for generating random bytes and shuffling lists
728+ using a deterministic algorithm seeded with a 256-bit key.
729+
730+ The implementation uses AES-256 in CTR mode, which is a well-established
731+ method for creating a CSPRNG.
732+ """
733+ cdef EVP_CIPHER_CTX * ctx
734+ cdef uint8_t key[32 ]
735+ cdef uint8_t iv[16 ]
736+ cdef uint8_t zeros[4096 ] # Static buffer for zeros
737+ cdef uint8_t buffer [4096 ] # Static buffer for random bytes
738+ cdef size_t buffer_size
739+ cdef size_t buffer_pos
740+
741+ def __cinit__ (self , bytes seed_key ):
742+ """
743+ Initialize the CSPRNG with a 256-bit key.
744+
745+ :param seed_key: A 32-byte key used as the seed for the CSPRNG
746+ """
747+ if len (seed_key) != 32 :
748+ raise ValueError (" Seed key must be 32 bytes (256 bits)" )
749+
750+ # Initialize context
751+ self .ctx = EVP_CIPHER_CTX_new()
752+ if self .ctx == NULL :
753+ raise MemoryError (" Failed to allocate cipher context" )
754+
755+ self .key = seed_key[:32 ]
756+
757+ # Initialize to zeros
758+ memset(self .iv, 0 , 16 )
759+ memset(self .zeros, 0 , 4096 )
760+
761+ self .buffer_size = 4096
762+ self .buffer_pos = self .buffer_size # Force refill on first use
763+
764+ # Initialize the cipher
765+ if not EVP_EncryptInit_ex(self .ctx, EVP_aes_256_ctr(), NULL , self .key, self .iv):
766+ EVP_CIPHER_CTX_free(self .ctx)
767+ raise CryptoError(" Failed to initialize AES-CTR cipher" )
768+
769+ def __dealloc__ (self ):
770+ """ Free resources when the object is deallocated."""
771+ if self .ctx != NULL :
772+ EVP_CIPHER_CTX_free(self .ctx)
773+ self .ctx = NULL
774+
775+ cdef _refill_buffer(self ):
776+ """ Refill the internal buffer with random bytes."""
777+ cdef int outlen = 0
778+
779+ # Encrypt zeros to get random bytes
780+ if not EVP_EncryptUpdate(self .ctx, self .buffer, & outlen, self .zeros, self .buffer_size):
781+ raise CryptoError(" Failed to generate random bytes" )
782+ if outlen != self .buffer_size:
783+ raise CryptoError(" Unexpected length of random bytes" )
784+
785+ self .buffer_pos = 0
786+
787+ def random_bytes (self , size_t n ):
788+ """
789+ Generate n random bytes.
790+
791+ :param n: Number of bytes to generate
792+ :return: a bytes object containing the random bytes
793+ """
794+ # Directly create a Python bytes object of the required size
795+ cdef object py_bytes = PyBytes_FromStringAndSize(NULL , n)
796+ cdef uint8_t * result = < uint8_t * > PyBytes_AsString(py_bytes)
797+ cdef size_t remaining
798+ cdef size_t pos
799+ cdef size_t to_copy
800+ cdef size_t available
801+
802+ remaining = n
803+ pos = 0
804+
805+ while remaining > 0 :
806+ if self .buffer_pos >= self .buffer_size:
807+ self ._refill_buffer()
808+
809+ # Calculate how many bytes we can copy
810+ available = self .buffer_size - self .buffer_pos
811+ to_copy = remaining if remaining < available else available
812+
813+ # Copy bytes from buffer to result
814+ memcpy(result + pos, & self .buffer[self .buffer_pos], to_copy)
815+
816+ self .buffer_pos += to_copy
817+ pos += to_copy
818+ remaining -= to_copy
819+
820+ return py_bytes
821+
822+ def random_int (self , n ):
823+ """
824+ Generate a random integer in the range [0, n).
825+
826+ :param n: Upper bound (exclusive)
827+ :return: Random integer
828+ """
829+ if n <= 0 :
830+ raise ValueError (" Upper bound must be positive" )
831+ if n == 1 :
832+ return 0
833+
834+ # Calculate the number of bits and bytes needed
835+ bits_needed = 0
836+ temp = n - 1
837+ while temp > 0 :
838+ bits_needed += 1
839+ temp >>= 1
840+ bytes_needed = (bits_needed + 7 ) // 8
841+
842+ # Generate random bytes
843+ mask = (1 << bits_needed) - 1
844+ max_attempts = 1000 # Prevent infinite loop
845+
846+ # Rejection sampling to avoid bias
847+ attempts = 0
848+ while attempts < max_attempts:
849+ attempts += 1
850+ random_data = self .random_bytes(bytes_needed)
851+ result = int .from_bytes(random_data, byteorder = ' big' )
852+
853+ # Apply mask to get the right number of bits
854+ result &= mask
855+ if result < n:
856+ return result
857+
858+ # If we reach here, we've made too many attempts
859+ # Fall back to a slightly biased but guaranteed-to-terminate method
860+ random_data = self .random_bytes(bytes_needed)
861+ result = int .from_bytes(random_data, byteorder = ' big' )
862+ return result % n
863+
864+ def shuffle (self , list items ):
865+ """
866+ Shuffle a list in-place using the Fisher-Yates algorithm.
867+
868+ :param items: List to shuffle
869+ """
870+ cdef size_t n = len (items)
871+ cdef size_t i, j
872+
873+ for i in range (n - 1 , 0 , - 1 ):
874+ # Generate random index j such that 0 <= j <= i
875+ j = self .random_int(i + 1 )
876+
877+ # Swap items[i] and items[j]
878+ items[i], items[j] = items[j], items[i]
0 commit comments