Added Python convenience for different nonce sizes. Statistical tests of output randomness.
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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]:
|
||||||
|
|||||||
Reference in New Issue
Block a user