random(): per-thread singleton; random_key/random_nonce use it

Each cipher module's random() now returns a thread-local Random that is
created on first use and reused thereafter. Each thread gets its own
OS-seeded generator (own key+nonce), so no locking and no cross-thread
nonce collisions. random_key() and random_nonce() draw from this CSPRNG
instead of secrets.token_bytes, making them as fast as the stream.
Changes made in the aegis256x4.py template and regenerated.
This commit is contained in:
LeoVasanko committed 2026-10-04 15:42:02 +00:00
1 parent 32954c4eba
commit 9d36d3b81b
9 files changed
+163 -64

No files matched your search

+2 -2
View File
@@ -128,9 +128,9 @@ Constants (per module): NAME, KEYBYTES, NONCEBYTES, MACBYTES, MACBYTES_LONG, RAT
### CSPRNG (aeg.random) ### CSPRNG (aeg.random)
Seeded once from the OS, then a deterministic AEGIS keystream: each call increments the nonce, so no output block ever repeats. Seeded once from the OS, then a deterministic AEGIS keystream: each call increments the nonce, so no output block ever repeats. random_key() and random_nonce() draw from it.
- ciph.random() -> aeg.random.Random bound to the cipher module - ciph.random() -> per-thread aeg.random.Random singleton for the cipher module
- rng.bytes(n) -> bytearray, rng.into(buf), rng(n) - rng.bytes(n) -> bytearray, rng.into(buf), rng(n)
### Random-access encrypted files (aeg.raf) ### Random-access encrypted files (aeg.raf)
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+17 -10
View File
@@ -3,8 +3,8 @@
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
import errno import errno
import secrets
import sys import sys
import threading
from typing import Literal from typing import Literal
from ._loader import ffi from ._loader import ffi
@@ -21,34 +21,41 @@ ALIGNMENT = 64 #: Required alignment for internal structures
RATE = 64 #: Byte chunk size in internal processing RATE = 64 #: Byte chunk size in internal processing
def random() -> Random: _random_tls = threading.local()
"""Return a new AEGIS stream Random instance using this cipher module.
Seeded once from the OS, then deterministic: successive calls never
repeat an output block. See aeg.random.Random for the API. def random() -> Random:
"""Return this module's Random instance (per-thread singleton).
Each thread gets its own generator on first use, seeded once from the
OS, then deterministic: successive calls never repeat an output block.
No locking is involved. See aeg.random.Random for the API.
""" """
return Random(sys.modules[__name__]) rng = getattr(_random_tls, "rng", None)
if rng is None:
rng = _random_tls.rng = Random(sys.modules[__name__])
return rng
def random_key() -> bytearray: def random_key() -> bytearray:
""" """
Generate a secret key using cryptographically secure random bytes. Generate a secret key using the module's CSPRNG (see random()).
It is recommended to wipe() the key after no longer needed. It is recommended to wipe() the key after no longer needed.
""" """
return bytearray(secrets.token_bytes(KEYBYTES)) return random().bytes(KEYBYTES)
def random_nonce() -> bytearray: def random_nonce() -> bytearray:
""" """
Generate a public nonce using cryptographically secure random bytes. Generate a public nonce using the module's CSPRNG (see random()).
Nonces (a number used once) are public data that may be sent together Nonces (a number used once) are public data that may be sent together
with the ciphertext, but they need to be unique for each use. with the ciphertext, but they need to be unique for each use.
See also: nonce_increment() can be used to derive sequential nonces. See also: nonce_increment() can be used to derive sequential nonces.
""" """
return bytearray(secrets.token_bytes(NONCEBYTES)) return random().bytes(NONCEBYTES)
def _ptr(buf): def _ptr(buf):
+3 -2
View File
@@ -31,8 +31,9 @@ class Random:
Each call produces keystream under the current key/nonce and increments Each call produces keystream under the current key/nonce and increments
the nonce afterwards, so no output block is ever produced twice. the nonce afterwards, so no output block is ever produced twice.
The primary way to create one is the cipher module's own random() The primary way to get one is the cipher module's own random() helper,
helper, e.g. ``aeg.cipher("AEGIS-128X2").random()``. e.g. ``aeg.cipher("AEGIS-128X2").random()``, which returns a per-thread
singleton (each thread gets its own OS-seeded generator; no locking).
""" """
def __init__(self, cipher, *, seed: Buffer | None = None): def __init__(self, cipher, *, seed: Buffer | None = None):
+56
View File
@@ -0,0 +1,56 @@
import threading
import pytest
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
CIPHERS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
def cipher_id(c):
return c.NAME
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
def test_random_is_per_thread_singleton(c):
assert c.random() is c.random()
other = []
t = threading.Thread(target=lambda: other.append(c.random()))
t.start()
t.join()
assert other[0] is not c.random()
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
def test_random_key_nonce_from_csprng(c):
assert len(c.random_key()) == c.KEYBYTES
assert len(c.random_nonce()) == c.NONCEBYTES
# successive outputs never repeat
assert c.random_key() != c.random_key()
assert c.random_nonce() != c.random_nonce()
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
def test_threads_get_independent_streams(c):
results = [None] * 2
def work(i):
results[i] = bytes(c.random_key())
threads = [threading.Thread(target=work, args=(i,)) for i in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
assert results[0] != results[1]
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
def test_bytes_into_callable(c):
rng = c.random()
a = rng.bytes(64)
buf = bytearray(64)
rng.into(buf)
b = rng(64)
assert len(a) == len(b) == 64
assert a != bytes(buf) != b # three consecutive blocks differ