Added Python convenience for different nonce sizes. Statistical tests of output randomness.

This commit is contained in:
2023-10-29 12:17:05 +00:00
parent baf3ef31a5
commit 4f42fb3447
3 changed files with 58 additions and 16 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs", "wheel", "cffi"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "RandQuik" name = "randquik"
version = "0.1.0" version = "0.1.0"
description = "Extremely fast and cryptographically secure random number generator." description = "Extremely fast and cryptographically secure random number generator."
readme = "README.md" readme = "README.md"
@@ -16,7 +16,7 @@ classifiers = [
"Topic :: Security", "Topic :: Security",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
dependencies = ["cffi>=1.0.1"] dependencies = ["cffi>=1.0.1", "numpy"]
requires-python = ">=3.10" requires-python = ">=3.10"
keywords = [ keywords = [
"random", "random",
@@ -30,7 +30,7 @@ keywords = [
[project.urls] [project.urls]
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest", "ruff", "cryptography"] dev = ["pytest", "ruff", "cryptography", "scipy"]
[tool.hatchling] [tool.hatchling]
+13 -5
View File
@@ -30,12 +30,20 @@ lib = ffi.dlopen(
def _processKeys(key, iv): def _processKeys(key, iv):
if len(key) != 32: key = memoryview(key)
iv = memoryview(iv)
if key.nbytes != 32:
raise ValueError("key must be 32 bytes") raise ValueError("key must be 32 bytes")
if len(iv) != 16: if iv.nbytes != 16:
raise ValueError( # Allow original and IETF nonces with zero counter
"iv must be full 16 bytes, starting with the counter - usually zeroes - followed by nonce" if iv.nbytes == 8:
) iv = bytes(8) + iv
elif iv.nbytes == 12:
iv = bytes(4) + iv
else:
raise ValueError(
"iv lenth must be 8 (original nonce), 12 (IETF) or 16 (counter in initial 8 bytes)"
)
return ffi.from_buffer(key), ffi.from_buffer(iv) return ffi.from_buffer(key), ffi.from_buffer(iv)
+42 -8
View File
@@ -1,17 +1,50 @@
from secrets import randbelow, token_bytes from secrets import randbelow, token_bytes
import numpy as np
from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20 from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20
from scipy.stats import chisquare
from randquik import cha from randquik import cha
def test_cha_generate_statistical():
"""Requests in multiple of ChaCha20 block size 64 bytes"""
ROUNDS = 100
min_p = 1 - 0.99 ** (1 / ROUNDS) # 99 % confidence over the entire test
print(min_p)
for round in range(ROUNDS):
# First round both zero, use 8-byte nonce as round counter
key = bytes(32)
nonce = round.to_bytes(8, "little")
iv = bytes(8) + nonce # Cryptography module requires IV (counter, nonce)
# Varying sizes to test internal processing that occurs in 64 bit blocks
# and with SIMD implementations also 256 or 512 bytes at a time.
N = 10000 + randbelow(10000)
ct0, ct1 = bytearray(N), bytearray(N)
Cipher(ChaCha20(key, iv), None, None).encryptor().update_into(bytes(N), ct0)
cha.generate(ct1, key, nonce)
assert len(ct0) == len(ct1)
assert ct0.hex() == ct1.hex()
assert ct1.count(0)
# Zeroing out 50 bytes fail the test in ~30 rounds
# ct1[:50] = bytes(50)
# Test that all byte values are equivalently common (despite zero inputs)
observed = np.bincount(ct1, minlength=256)
expected = np.full(256, N / 256)
chi2, p_value = chisquare(observed, expected)
assert p_value >= min_p, f"{round=} {N=}"
def test_cipherstreams_fullblocks(): def test_cipherstreams_fullblocks():
"""Requests in multiple of ChaCha20 block size 64 bytes""" """Requests in multiple of ChaCha20 block size 64 bytes"""
key = token_bytes(32) key = token_bytes(32)
nonce = token_bytes(16) iv = token_bytes(16)
c0 = Cipher(ChaCha20(key, nonce), None, None).encryptor() c0 = Cipher(ChaCha20(key, iv), None, None).encryptor()
c1 = cha.Cha(key, nonce) c1 = cha.Cha(key, iv)
for i in range(2048): for i in range(2048):
N = 64 * (1 + randbelow(2048)) N = 64 * (1 + randbelow(2048))
@@ -24,9 +57,9 @@ def test_cipherstreams_fullblocks():
def test_cipherstreams_partial_updates(): def test_cipherstreams_partial_updates():
"""Odd-sized requests that retain leftover buffers""" """Odd-sized requests that retain leftover buffers"""
key = token_bytes(32) key = token_bytes(32)
nonce = token_bytes(16) iv = token_bytes(16)
c0 = Cipher(ChaCha20(key, nonce), None, None).encryptor() c0 = Cipher(ChaCha20(key, iv), None, None).encryptor()
c1 = cha.Cha(key, nonce) c1 = cha.Cha(key, iv)
for i in range(2048): for i in range(2048):
N = 1 + randbelow(2048) N = 1 + randbelow(2048)
@@ -39,8 +72,9 @@ def test_cipherstreams_partial_updates():
def test_cipherstreams_32leftover(): def test_cipherstreams_32leftover():
"""Test the special case where the second response comes entirely from the leftover buffer""" """Test the special case where the second response comes entirely from the leftover buffer"""
key = token_bytes(32) key = token_bytes(32)
nonce = token_bytes(16) nonce = token_bytes(12) # IETF nonce size
c0 = Cipher(ChaCha20(key, nonce), None, None).encryptor() iv = bytes(4) + nonce
c0 = Cipher(ChaCha20(key, iv), None, None).encryptor()
c1 = cha.Cha(key, nonce) c1 = cha.Cha(key, nonce)
for N in [512 - 32, 32]: for N in [512 - 32, 32]: