Files
quantumtunnel/tests/test_chab3.py
T
2026-10-07 03:07:34 +00:00

168 lines
4.8 KiB
Python

import secrets
import pytest
from quantumpipe.chab3 import (
NONCE_LEN,
Receiver,
Sender,
decrypt,
decrypt_into,
encrypt,
encrypt_into,
)
KEY = secrets.token_bytes(32)
NONCE = secrets.token_bytes(12)
AD = secrets.token_bytes(7)
MSG = b"streaming payload, split " + b"into two chunks"
def test_roundtrip():
ct = encrypt(MSG, KEY, NONCE, ad=AD)
assert isinstance(ct, bytearray)
assert len(ct) == len(MSG) + 16
pt = decrypt(ct, KEY, NONCE, ad=AD)
assert isinstance(pt, bytearray)
assert pt == MSG
def test_into_variants():
ref = encrypt(MSG, KEY, NONCE, ad=AD)
buf = bytearray(len(MSG) + 16 + 10) # oversized buffer
used = encrypt_into(buf, MSG, KEY, NONCE, ad=AD)
assert bytes(used) == ref
assert buf[len(used) :] == bytes(10) # tail untouched
out = bytearray(len(MSG) + 5)
pt = decrypt_into(out, used, KEY, NONCE, ad=AD)
assert len(pt) == len(MSG)
assert pt == MSG
def test_inplace():
buf = bytearray(MSG + bytes(16))
ct = encrypt_into(buf, buf[: len(MSG)], KEY, NONCE, ad=AD)
assert bytes(ct) == encrypt(MSG, KEY, NONCE, ad=AD)
pt = decrypt_into(buf, buf, KEY, NONCE, ad=AD)
assert pt == MSG
def test_maclen_zero():
ct = encrypt(MSG, KEY, NONCE, ad=AD, maclen=0)
assert len(ct) == len(MSG) # no tag appended
assert decrypt(ct, KEY, NONCE, maclen=0) == MSG
box = encrypt(MSG, KEY, maclen=0)
assert len(box) == NONCE_LEN + len(MSG)
assert decrypt(box, KEY, maclen=0) == MSG
@pytest.mark.parametrize(
"fn",
[
lambda: encrypt_into(bytearray(1), MSG, KEY, NONCE, ad=AD),
lambda: decrypt_into(
bytearray(1), encrypt(MSG, KEY, NONCE, ad=AD), KEY, NONCE, ad=AD
),
],
)
def test_too_small_output_buffer_raises_type_error(fn):
# a program bug, not bad wire data
with pytest.raises(TypeError):
fn()
@pytest.mark.parametrize(
"fn",
[
lambda: decrypt(b"short", KEY, NONCE, ad=AD),
lambda: decrypt(b"short", KEY, ad=AD),
],
)
def test_short_ciphertext_raises_value_error(fn):
# bad wire data, not a program bug
with pytest.raises(ValueError):
fn()
def test_tampering_raises_value_error():
good = bytes(encrypt(MSG, KEY, NONCE, ad=AD))
body, tag = good[:-16], good[-16:]
cases = [
(bytes([body[0] ^ 1]) + body[1:] + tag, AD), # tampered ciphertext
(good, AD + b"x"), # wrong ad
(body + bytes([tag[0] ^ 1]) + bytes(tag[1:]), AD), # wrong tag
]
for ct, ad in cases:
with pytest.raises(ValueError):
decrypt(ct, KEY, NONCE, ad=ad)
def test_boxes():
box1, box2 = encrypt(MSG, KEY, ad=AD), encrypt(MSG, KEY, ad=AD)
assert box1 != box2 # fresh random nonce each time
assert len(box1) == NONCE_LEN + len(MSG) + 16
assert decrypt(box1, KEY, ad=AD) == MSG
buf = bytearray(len(box1))
encrypt_into(buf, MSG, KEY, ad=AD)
out = bytearray(len(MSG))
decrypt_into(out, buf, KEY, ad=AD)
assert bytes(out) == MSG
tampered = bytearray(box1)
tampered[-1] ^= 1
with pytest.raises(ValueError):
decrypt(bytes(tampered), KEY, ad=AD)
def test_decrypt_into_verifies_before_writing():
ct = bytearray(encrypt(MSG, KEY, NONCE, ad=AD))
ct[-1] ^= 1 # bad tag
out = bytearray(b"\xaa" * len(MSG))
with pytest.raises(ValueError):
decrypt_into(out, ct, KEY, NONCE, ad=AD)
assert out == bytearray(b"\xaa" * len(MSG)) # untouched
def test_channel_in_order():
tx, rx = Sender(KEY), Receiver(KEY)
for i in range(3):
ct = tx.encrypt(f"msg {i}".encode(), ad=AD)
assert rx.decrypt(ct, ad=AD) == f"msg {i}".encode()
def test_channel_into():
tx, rx = Sender(KEY), Receiver(KEY)
buf = bytearray(len(MSG) + 16)
ct = tx.encrypt_into(buf, MSG, ad=AD)
out = bytearray(len(MSG))
assert rx.decrypt_into(out, ct, ad=AD) == MSG
def test_channel_desync_rejected():
tx = Sender(KEY)
for _ in range(3):
tx.encrypt(b"x")
with pytest.raises(ValueError):
Receiver(KEY).decrypt(tx.encrypt(b"out of order")) # nonce 0 vs 3
def test_random_reseeds_on_counter_exhaustion():
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20
import quantumpipe.chab3 as m
exhausted = Cipher(
ChaCha20(bytes(32), (0xFFFFFFFF).to_bytes(4, "little") + bytes(12)),
mode=None,
).encryptor()
m._random.enc = exhausted
out = m.random(128) # needs 2 blocks; the planted stream has 1 left
assert len(out) == 128
assert m._random.enc is not exhausted
buf = bytearray(128)
m._random.enc = exhausted
m.random(buf)
assert m._random.enc is not exhausted
assert buf != bytes(128)
del m._random.enc # don't leak the fixture into other tests