initial commit
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
This commit is contained in:
commit
1d99048c95
165830 changed files with 79062 additions and 0 deletions
27
soliton_py/tests/test_auth.py
Normal file
27
soliton_py/tests/test_auth.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""Tests for LO-Auth challenge-response."""
|
||||
|
||||
import soliton
|
||||
|
||||
|
||||
def test_auth_round_trip():
|
||||
with soliton.Identity.generate() as id:
|
||||
pk = id.public_key()
|
||||
sk = id.secret_key()
|
||||
|
||||
# Server generates challenge.
|
||||
ct, token = soliton.auth_challenge(pk)
|
||||
|
||||
# Client responds.
|
||||
proof = soliton.auth_respond(sk, ct)
|
||||
|
||||
# Server verifies.
|
||||
assert soliton.auth_verify(token, proof) is True
|
||||
|
||||
|
||||
def test_auth_wrong_proof():
|
||||
with soliton.Identity.generate() as id:
|
||||
pk = id.public_key()
|
||||
ct, token = soliton.auth_challenge(pk)
|
||||
# Tampered proof.
|
||||
fake_proof = b"\x00" * 32
|
||||
assert soliton.auth_verify(token, fake_proof) is False
|
||||
78
soliton_py/tests/test_identity.py
Normal file
78
soliton_py/tests/test_identity.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Tests for identity key management."""
|
||||
|
||||
import soliton
|
||||
|
||||
|
||||
def test_keygen():
|
||||
with soliton.Identity.generate() as id:
|
||||
pk = id.public_key()
|
||||
sk = id.secret_key()
|
||||
assert len(pk) == 3200 # SOLITON_PUBLIC_KEY_SIZE
|
||||
assert len(sk) == 2496 # SOLITON_SECRET_KEY_SIZE
|
||||
|
||||
|
||||
def test_fingerprint():
|
||||
with soliton.Identity.generate() as id:
|
||||
fp = id.fingerprint()
|
||||
assert len(fp) == 32
|
||||
assert fp != b"\x00" * 32
|
||||
|
||||
|
||||
def test_fingerprint_hex():
|
||||
with soliton.Identity.generate() as id:
|
||||
hex_fp = id.fingerprint_hex()
|
||||
assert len(hex_fp) == 64
|
||||
|
||||
|
||||
def test_sign_verify():
|
||||
with soliton.Identity.generate() as id:
|
||||
msg = b"test message"
|
||||
sig = id.sign(msg)
|
||||
assert len(sig) == 3373 # SOLITON_HYBRID_SIG_SIZE
|
||||
# Verify with same identity.
|
||||
id.verify(msg, sig)
|
||||
|
||||
|
||||
def test_sign_verify_wrong_message():
|
||||
with soliton.Identity.generate() as id:
|
||||
sig = id.sign(b"correct")
|
||||
try:
|
||||
id.verify(b"wrong", sig)
|
||||
assert False, "should have raised"
|
||||
except soliton.VerificationError:
|
||||
pass
|
||||
|
||||
|
||||
def test_context_manager_zeroizes():
|
||||
id = soliton.Identity.generate()
|
||||
with id:
|
||||
_ = id.secret_key()
|
||||
# After exiting context, secret key should be gone.
|
||||
try:
|
||||
id.secret_key()
|
||||
assert False, "should have raised"
|
||||
except soliton.InvalidDataError:
|
||||
pass
|
||||
|
||||
|
||||
def test_from_bytes_roundtrip():
|
||||
with soliton.Identity.generate() as id:
|
||||
pk = id.public_key()
|
||||
sk = id.secret_key()
|
||||
# Reconstruct.
|
||||
id2 = soliton.Identity.from_bytes(pk, sk)
|
||||
msg = b"roundtrip"
|
||||
sig = id2.sign(msg)
|
||||
id2.verify(msg, sig)
|
||||
id2.close()
|
||||
|
||||
|
||||
def test_public_only_cannot_sign():
|
||||
with soliton.Identity.generate() as id:
|
||||
pk = id.public_key()
|
||||
pub_only = soliton.Identity.from_public_bytes(pk)
|
||||
try:
|
||||
pub_only.sign(b"test")
|
||||
assert False, "should have raised"
|
||||
except soliton.InvalidDataError:
|
||||
pass
|
||||
326
soliton_py/tests/test_kex_ratchet.py
Normal file
326
soliton_py/tests/test_kex_ratchet.py
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
"""Tests for KEX, ratchet, and call keys — full session lifecycle."""
|
||||
|
||||
import os
|
||||
import soliton
|
||||
|
||||
|
||||
def test_first_message_round_trip():
|
||||
ck = os.urandom(32)
|
||||
aad = b"test-aad"
|
||||
pt = b"first application message"
|
||||
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(ck, pt, aad)
|
||||
decrypted, rik_b = soliton.Ratchet.decrypt_first_message(ck, ct, aad)
|
||||
|
||||
assert decrypted == pt
|
||||
assert rik_a == rik_b
|
||||
|
||||
|
||||
def test_first_message_wrong_key():
|
||||
ck = os.urandom(32)
|
||||
ct, _ = soliton.Ratchet.encrypt_first_message(ck, b"hello", b"aad")
|
||||
try:
|
||||
soliton.Ratchet.decrypt_first_message(os.urandom(32), ct, b"aad")
|
||||
assert False, "should have raised AeadError"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_first_message_wrong_aad():
|
||||
ck = os.urandom(32)
|
||||
ct, _ = soliton.Ratchet.encrypt_first_message(ck, b"hello", b"aad1")
|
||||
try:
|
||||
soliton.Ratchet.decrypt_first_message(ck, ct, b"aad2")
|
||||
assert False, "should have raised AeadError"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_first_message_empty_plaintext():
|
||||
ck = os.urandom(32)
|
||||
ct, rik = soliton.Ratchet.encrypt_first_message(ck, b"", b"aad")
|
||||
pt, rik2 = soliton.Ratchet.decrypt_first_message(ck, ct, b"aad")
|
||||
assert pt == b""
|
||||
assert rik == rik2
|
||||
|
||||
|
||||
def test_kex_sign_verify_bundle():
|
||||
bob = soliton.Identity.generate()
|
||||
spk_pub, spk_sk = soliton.xwing_keygen()
|
||||
|
||||
sig = soliton.kex_sign_prekey(bob.secret_key(), spk_pub)
|
||||
assert len(sig) == 3373
|
||||
|
||||
soliton.kex_verify_bundle(
|
||||
bob.public_key(), bob.public_key(),
|
||||
spk_pub, 1, sig, "lo-crypto-v1",
|
||||
)
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_kex_verify_bundle_wrong_key():
|
||||
bob = soliton.Identity.generate()
|
||||
eve = soliton.Identity.generate()
|
||||
spk_pub, _ = soliton.xwing_keygen()
|
||||
sig = soliton.kex_sign_prekey(bob.secret_key(), spk_pub)
|
||||
|
||||
try:
|
||||
soliton.kex_verify_bundle(
|
||||
bob.public_key(), eve.public_key(),
|
||||
spk_pub, 1, sig, "lo-crypto-v1",
|
||||
)
|
||||
assert False, "should have raised"
|
||||
except soliton.BundleVerificationError:
|
||||
pass
|
||||
bob.close()
|
||||
eve.close()
|
||||
|
||||
|
||||
def _full_kex():
|
||||
"""Run a complete KEX and return everything needed for ratchet init."""
|
||||
alice = soliton.Identity.generate()
|
||||
bob = soliton.Identity.generate()
|
||||
spk_pub, spk_sk = soliton.xwing_keygen()
|
||||
spk_sig = soliton.kex_sign_prekey(bob.secret_key(), spk_pub)
|
||||
|
||||
initiated = soliton.kex_initiate(
|
||||
alice.public_key(), alice.secret_key(),
|
||||
bob.public_key(), spk_pub, 1, spk_sig, "lo-crypto-v1",
|
||||
)
|
||||
|
||||
si_encoded = initiated.session_init_encoded()
|
||||
received = soliton.kex_receive(
|
||||
bob.public_key(), bob.secret_key(), alice.public_key(),
|
||||
si_encoded, initiated.sender_sig(), spk_sk,
|
||||
)
|
||||
|
||||
return alice, bob, initiated, received, si_encoded
|
||||
|
||||
|
||||
def test_full_kex_round_trip():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
|
||||
# First message.
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(),
|
||||
initiated.recipient_fingerprint(),
|
||||
si_encoded,
|
||||
)
|
||||
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(
|
||||
initiated.take_initial_chain_key(), b"hello bob", aad,
|
||||
)
|
||||
pt, rik_b = soliton.Ratchet.decrypt_first_message(
|
||||
received.take_initial_chain_key(), ct, aad,
|
||||
)
|
||||
assert pt == b"hello bob"
|
||||
assert rik_a == rik_b
|
||||
|
||||
# Init ratchets.
|
||||
alice_ratchet = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
received.peer_ek(), initiated.ek_sk(),
|
||||
)
|
||||
bob_ratchet = soliton.Ratchet.init_bob(
|
||||
received.take_root_key(), rik_b,
|
||||
bob.fingerprint(), alice.fingerprint(),
|
||||
received.peer_ek(),
|
||||
)
|
||||
|
||||
# Alice sends to Bob.
|
||||
hdr, ct = alice_ratchet.encrypt(b"message 1")
|
||||
assert bob_ratchet.decrypt(hdr, ct) == b"message 1"
|
||||
|
||||
# Bob replies (direction change — KEM ratchet step).
|
||||
hdr2, ct2 = bob_ratchet.encrypt(b"reply 1")
|
||||
assert alice_ratchet.decrypt(hdr2, ct2) == b"reply 1"
|
||||
|
||||
# Multiple messages same direction.
|
||||
hdr3, ct3 = alice_ratchet.encrypt(b"message 2")
|
||||
hdr4, ct4 = alice_ratchet.encrypt(b"message 3")
|
||||
assert bob_ratchet.decrypt(hdr3, ct3) == b"message 2"
|
||||
assert bob_ratchet.decrypt(hdr4, ct4) == b"message 3"
|
||||
|
||||
alice_ratchet.close()
|
||||
bob_ratchet.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_ratchet_duplicate_rejected():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(), initiated.recipient_fingerprint(), si_encoded,
|
||||
)
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(initiated.take_initial_chain_key(), b"x", aad)
|
||||
_, rik_b = soliton.Ratchet.decrypt_first_message(received.take_initial_chain_key(), ct, aad)
|
||||
|
||||
alice_r = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
received.peer_ek(), initiated.ek_sk(),
|
||||
)
|
||||
bob_r = soliton.Ratchet.init_bob(
|
||||
received.take_root_key(), rik_b,
|
||||
bob.fingerprint(), alice.fingerprint(),
|
||||
received.peer_ek(),
|
||||
)
|
||||
|
||||
hdr, ct = alice_r.encrypt(b"unique")
|
||||
assert bob_r.decrypt(hdr, ct) == b"unique"
|
||||
|
||||
# Replay must fail.
|
||||
try:
|
||||
bob_r.decrypt(hdr, ct)
|
||||
assert False, "should have raised DuplicateMessageError"
|
||||
except soliton.DuplicateMessageError:
|
||||
pass
|
||||
|
||||
alice_r.close()
|
||||
bob_r.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_ratchet_serialize_deserialize():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(), initiated.recipient_fingerprint(), si_encoded,
|
||||
)
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(initiated.take_initial_chain_key(), b"x", aad)
|
||||
_, rik_b = soliton.Ratchet.decrypt_first_message(received.take_initial_chain_key(), ct, aad)
|
||||
|
||||
alice_r = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
received.peer_ek(), initiated.ek_sk(),
|
||||
)
|
||||
|
||||
# Send a message to advance state.
|
||||
hdr, ct = alice_r.encrypt(b"before serialize")
|
||||
blob, epoch = alice_r.to_bytes()
|
||||
assert len(blob) > 0
|
||||
assert epoch >= 1
|
||||
|
||||
# Deserialize.
|
||||
restored = soliton.Ratchet.from_bytes(blob, 0)
|
||||
hdr2, ct2 = restored.encrypt(b"after restore")
|
||||
assert len(ct2) > 0
|
||||
|
||||
restored.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_derive_call_keys():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(), initiated.recipient_fingerprint(), si_encoded,
|
||||
)
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(initiated.take_initial_chain_key(), b"x", aad)
|
||||
_, rik_b = soliton.Ratchet.decrypt_first_message(received.take_initial_chain_key(), ct, aad)
|
||||
|
||||
alice_r = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
received.peer_ek(), initiated.ek_sk(),
|
||||
)
|
||||
|
||||
kem_ss = os.urandom(32)
|
||||
call_id = os.urandom(16)
|
||||
|
||||
with alice_r.derive_call_keys(kem_ss, call_id) as keys:
|
||||
send = keys.send_key()
|
||||
recv = keys.recv_key()
|
||||
assert len(send) == 32
|
||||
assert len(recv) == 32
|
||||
assert send != recv
|
||||
keys.advance()
|
||||
send2 = keys.send_key()
|
||||
assert send2 != send # keys changed after advance
|
||||
|
||||
alice_r.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_ratchet_can_serialize_and_epoch():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(), initiated.recipient_fingerprint(), si_encoded,
|
||||
)
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(initiated.take_initial_chain_key(), b"x", aad)
|
||||
_, rik_b = soliton.Ratchet.decrypt_first_message(received.take_initial_chain_key(), ct, aad)
|
||||
|
||||
alice_r = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
initiated.ek_pk(), initiated.ek_sk(),
|
||||
)
|
||||
|
||||
assert alice_r.can_serialize()
|
||||
assert alice_r.epoch() == 0
|
||||
|
||||
alice_r.encrypt(b"advance")
|
||||
assert alice_r.epoch() == 0 # same-direction doesn't change epoch
|
||||
assert alice_r.can_serialize()
|
||||
|
||||
alice_r.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_ratchet_reset():
|
||||
alice, bob, initiated, received, si_encoded = _full_kex()
|
||||
aad = soliton.kex_build_first_message_aad(
|
||||
initiated.sender_fingerprint(), initiated.recipient_fingerprint(), si_encoded,
|
||||
)
|
||||
ct, rik_a = soliton.Ratchet.encrypt_first_message(initiated.take_initial_chain_key(), b"x", aad)
|
||||
_, _ = soliton.Ratchet.decrypt_first_message(received.take_initial_chain_key(), ct, aad)
|
||||
|
||||
alice_r = soliton.Ratchet.init_alice(
|
||||
initiated.take_root_key(), rik_a,
|
||||
alice.fingerprint(), bob.fingerprint(),
|
||||
initiated.ek_pk(), initiated.ek_sk(),
|
||||
)
|
||||
|
||||
alice_r.encrypt(b"before reset")
|
||||
alice_r.reset()
|
||||
|
||||
# Encrypt after reset should fail.
|
||||
try:
|
||||
alice_r.encrypt(b"after reset")
|
||||
assert False, "should have raised"
|
||||
except soliton.InvalidDataError:
|
||||
pass
|
||||
|
||||
alice_r.close()
|
||||
initiated.close()
|
||||
received.close()
|
||||
alice.close()
|
||||
bob.close()
|
||||
|
||||
|
||||
def test_verification_phrase_symmetric():
|
||||
alice = soliton.Identity.generate()
|
||||
bob = soliton.Identity.generate()
|
||||
|
||||
phrase1 = soliton.verification_phrase(alice.public_key(), bob.public_key())
|
||||
phrase2 = soliton.verification_phrase(bob.public_key(), alice.public_key())
|
||||
|
||||
assert len(phrase1) > 0
|
||||
assert phrase1 == phrase2
|
||||
|
||||
alice.close()
|
||||
bob.close()
|
||||
55
soliton_py/tests/test_primitives.py
Normal file
55
soliton_py/tests/test_primitives.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Tests for primitive cryptographic operations."""
|
||||
|
||||
import soliton
|
||||
|
||||
|
||||
def test_sha3_256_known_vector():
|
||||
"""FIPS 202 test vector: SHA3-256("abc")."""
|
||||
result = soliton.sha3_256(b"abc")
|
||||
expected = bytes.fromhex(
|
||||
"3a985da74fe225b2045c172d6bd390bd"
|
||||
"855f086e3e9d525b46bfe24511431532"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_sha3_256_empty():
|
||||
"""FIPS 202 test vector: SHA3-256("")."""
|
||||
result = soliton.sha3_256(b"")
|
||||
expected = bytes.fromhex(
|
||||
"a7ffc6f8bf1ed76651c14756a061d662"
|
||||
"f580ff4de43b49fa82d80a4b80f8434a"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_fingerprint_hex():
|
||||
result = soliton.fingerprint_hex(b"abc")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64 # 32 bytes hex-encoded
|
||||
|
||||
|
||||
def test_hmac_sha3_256():
|
||||
key = b"\x0b" * 32
|
||||
data = b"Hi There"
|
||||
tag = soliton.hmac_sha3_256(key, data)
|
||||
assert len(tag) == 32
|
||||
|
||||
|
||||
def test_hmac_sha3_256_verify():
|
||||
key = b"\x0b" * 32
|
||||
data = b"Hi There"
|
||||
tag = soliton.hmac_sha3_256(key, data)
|
||||
assert soliton.hmac_sha3_256_verify(tag, tag) is True
|
||||
# Tampered tag should fail.
|
||||
tampered = bytearray(tag)
|
||||
tampered[0] ^= 0xFF
|
||||
assert soliton.hmac_sha3_256_verify(tag, bytes(tampered)) is False
|
||||
|
||||
|
||||
def test_hkdf_sha3_256():
|
||||
salt = b"\x00" * 32
|
||||
ikm = b"\x0b" * 32
|
||||
info = b"test"
|
||||
okm = soliton.hkdf_sha3_256(salt, ikm, info, length=64)
|
||||
assert len(okm) == 64
|
||||
102
soliton_py/tests/test_storage.py
Normal file
102
soliton_py/tests/test_storage.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Tests for encrypted storage."""
|
||||
|
||||
import os
|
||||
import soliton
|
||||
|
||||
|
||||
def _random_key():
|
||||
return os.urandom(32)
|
||||
|
||||
|
||||
def test_storage_round_trip():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
pt = b"encrypted storage data"
|
||||
blob = ring.encrypt_blob("channel-1", "segment-0", pt)
|
||||
result = ring.decrypt_blob("channel-1", "segment-0", blob)
|
||||
assert result == pt
|
||||
|
||||
|
||||
def test_storage_wrong_channel_fails():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
blob = ring.encrypt_blob("channel-1", "seg", b"data")
|
||||
try:
|
||||
ring.decrypt_blob("channel-2", "seg", blob)
|
||||
assert False, "should have raised AeadError"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_storage_wrong_segment_fails():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
blob = ring.encrypt_blob("ch", "seg-1", b"data")
|
||||
try:
|
||||
ring.decrypt_blob("ch", "seg-2", blob)
|
||||
assert False, "should have raised AeadError"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_storage_key_rotation():
|
||||
key1 = _random_key()
|
||||
key2 = _random_key()
|
||||
with soliton.StorageKeyRing(1, key1) as ring:
|
||||
blob_v1 = ring.encrypt_blob("ch", "seg", b"v1 data")
|
||||
ring.add_key(2, key2, make_active=True)
|
||||
blob_v2 = ring.encrypt_blob("ch", "seg", b"v2 data")
|
||||
# Both decrypt (both keys in ring).
|
||||
assert ring.decrypt_blob("ch", "seg", blob_v1) == b"v1 data"
|
||||
assert ring.decrypt_blob("ch", "seg", blob_v2) == b"v2 data"
|
||||
|
||||
|
||||
def test_dm_queue_round_trip():
|
||||
fp = os.urandom(32)
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
pt = b"queued DM"
|
||||
blob = ring.encrypt_dm_queue(fp, "batch-1", pt)
|
||||
result = ring.decrypt_dm_queue(fp, "batch-1", blob)
|
||||
assert result == pt
|
||||
|
||||
|
||||
def test_dm_queue_wrong_fingerprint_fails():
|
||||
fp1 = os.urandom(32)
|
||||
fp2 = os.urandom(32)
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
blob = ring.encrypt_dm_queue(fp1, "batch", b"msg")
|
||||
try:
|
||||
ring.decrypt_dm_queue(fp2, "batch", blob)
|
||||
assert False, "should have raised AeadError"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_storage_empty_plaintext():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
blob = ring.encrypt_blob("ch", "seg", b"")
|
||||
assert ring.decrypt_blob("ch", "seg", blob) == b""
|
||||
|
||||
|
||||
def test_storage_compress():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
data = b"compressible " * 100
|
||||
blob = ring.encrypt_blob("ch", "seg", data, compress=True)
|
||||
assert ring.decrypt_blob("ch", "seg", blob) == data
|
||||
|
||||
|
||||
def test_remove_key():
|
||||
with soliton.StorageKeyRing(1, _random_key()) as ring:
|
||||
ring.add_key(2, _random_key(), make_active=True)
|
||||
ring.remove_key(1)
|
||||
blob = ring.encrypt_blob("ch", "seg", b"after remove")
|
||||
assert ring.decrypt_blob("ch", "seg", blob) == b"after remove"
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
ring = soliton.StorageKeyRing(1, _random_key())
|
||||
with ring:
|
||||
ring.encrypt_blob("ch", "seg", b"test")
|
||||
# After exit, operations should fail.
|
||||
try:
|
||||
ring.encrypt_blob("ch", "seg", b"test")
|
||||
assert False, "should have raised"
|
||||
except soliton.InvalidDataError:
|
||||
pass
|
||||
161
soliton_py/tests/test_stream.py
Normal file
161
soliton_py/tests/test_stream.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""Tests for streaming AEAD."""
|
||||
|
||||
import os
|
||||
import soliton
|
||||
|
||||
|
||||
CHUNK_SIZE = 1_048_576 # 1 MiB
|
||||
|
||||
|
||||
def _random_key():
|
||||
return os.urandom(32)
|
||||
|
||||
|
||||
def test_stream_single_chunk():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
assert len(header) == 26
|
||||
ct = enc.encrypt_chunk(b"hello stream", is_last=True)
|
||||
assert enc.is_finalized()
|
||||
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
pt, is_last = dec.decrypt_chunk(ct)
|
||||
assert pt == b"hello stream"
|
||||
assert is_last
|
||||
|
||||
|
||||
def test_stream_multi_chunk():
|
||||
key = _random_key()
|
||||
chunk1_data = os.urandom(CHUNK_SIZE) # non-final must be 1 MiB
|
||||
chunk2_data = b"final chunk"
|
||||
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
ct1 = enc.encrypt_chunk(chunk1_data, is_last=False)
|
||||
ct2 = enc.encrypt_chunk(chunk2_data, is_last=True)
|
||||
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
pt1, last1 = dec.decrypt_chunk(ct1)
|
||||
assert pt1 == chunk1_data
|
||||
assert not last1
|
||||
|
||||
pt2, last2 = dec.decrypt_chunk(ct2)
|
||||
assert pt2 == chunk2_data
|
||||
assert last2
|
||||
|
||||
|
||||
def test_stream_with_aad():
|
||||
key = _random_key()
|
||||
aad = b"file-id:12345"
|
||||
|
||||
with soliton.StreamEncryptor(key, aad=aad) as enc:
|
||||
header = enc.header()
|
||||
ct = enc.encrypt_chunk(b"aad-bound data", is_last=True)
|
||||
|
||||
# Correct AAD decrypts.
|
||||
with soliton.StreamDecryptor(key, header, aad=aad) as dec:
|
||||
pt, _ = dec.decrypt_chunk(ct)
|
||||
assert pt == b"aad-bound data"
|
||||
|
||||
# Wrong AAD fails.
|
||||
try:
|
||||
with soliton.StreamDecryptor(key, header, aad=b"wrong") as dec:
|
||||
dec.decrypt_chunk(ct)
|
||||
assert False, "should have raised"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_stream_wrong_key_fails():
|
||||
key1 = _random_key()
|
||||
key2 = _random_key()
|
||||
with soliton.StreamEncryptor(key1) as enc:
|
||||
header = enc.header()
|
||||
ct = enc.encrypt_chunk(b"data", is_last=True)
|
||||
|
||||
try:
|
||||
with soliton.StreamDecryptor(key2, header) as dec:
|
||||
dec.decrypt_chunk(ct)
|
||||
assert False, "should have raised"
|
||||
except soliton.AeadError:
|
||||
pass
|
||||
|
||||
|
||||
def test_stream_tampered_ciphertext():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
ct = bytearray(enc.encrypt_chunk(b"data", is_last=True))
|
||||
ct[0] ^= 0xFF
|
||||
|
||||
try:
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
dec.decrypt_chunk(bytes(ct))
|
||||
assert False, "should have raised"
|
||||
except (soliton.AeadError, soliton.InvalidDataError):
|
||||
pass
|
||||
|
||||
|
||||
def test_stream_empty_plaintext():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
ct = enc.encrypt_chunk(b"", is_last=True)
|
||||
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
pt, is_last = dec.decrypt_chunk(ct)
|
||||
assert pt == b""
|
||||
assert is_last
|
||||
|
||||
|
||||
def test_stream_encrypt_at_decrypt_at():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
# Short plaintext requires is_last=True (non-final must be exactly 1 MiB).
|
||||
ct0 = enc.encrypt_chunk_at(0, b"at index zero", is_last=True)
|
||||
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
pt0, last0 = dec.decrypt_chunk_at(0, ct0)
|
||||
assert pt0 == b"at index zero"
|
||||
assert last0
|
||||
|
||||
|
||||
def test_stream_decrypt_at_different_indices():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
ct5 = enc.encrypt_chunk_at(5, b"at five", is_last=True)
|
||||
ct99 = enc.encrypt_chunk_at(99, b"at ninety-nine", is_last=True)
|
||||
|
||||
# Decrypt in reverse order.
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
pt99, _ = dec.decrypt_chunk_at(99, ct99)
|
||||
assert pt99 == b"at ninety-nine"
|
||||
|
||||
pt5, _ = dec.decrypt_chunk_at(5, ct5)
|
||||
assert pt5 == b"at five"
|
||||
|
||||
|
||||
def test_stream_expected_index():
|
||||
key = _random_key()
|
||||
with soliton.StreamEncryptor(key) as enc:
|
||||
header = enc.header()
|
||||
ct = enc.encrypt_chunk(b"chunk", is_last=True)
|
||||
|
||||
with soliton.StreamDecryptor(key, header) as dec:
|
||||
assert dec.expected_index() == 0
|
||||
dec.decrypt_chunk(ct)
|
||||
assert dec.expected_index() == 1
|
||||
|
||||
|
||||
def test_stream_context_manager():
|
||||
enc = soliton.StreamEncryptor(_random_key())
|
||||
with enc:
|
||||
enc.header()
|
||||
try:
|
||||
enc.header()
|
||||
assert False, "should have raised"
|
||||
except soliton.InvalidDataError:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue