aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests/test_senderkeys.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common/tests/test_senderkeys.py')
-rw-r--r--packages/meshbay-common/tests/test_senderkeys.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/packages/meshbay-common/tests/test_senderkeys.py b/packages/meshbay-common/tests/test_senderkeys.py
new file mode 100644
index 0000000..a1181e1
--- /dev/null
+++ b/packages/meshbay-common/tests/test_senderkeys.py
@@ -0,0 +1,211 @@
+"""
+Tests for the Sender Keys group messaging protocol.
+
+Covers: key creation, distribution, encrypt/decrypt, multi-member groups,
+out-of-order delivery, serialization, and key rotation on member removal.
+"""
+
+import pytest
+
+from meshbay_common.senderkeys import (
+ SenderKeyRecord,
+ SenderKeyDistribution,
+ SenderKeyMessage,
+ GroupSenderKeyStore,
+ encrypt_message,
+ decrypt_message,
+)
+
+
+def test_basic_encrypt_decrypt():
+ """Alice encrypts, Bob decrypts using Alice's distributed sender key."""
+ alice_rec = SenderKeyRecord.create("alice")
+ alice_dist = alice_rec.distribution()
+
+ bob_store = GroupSenderKeyStore("group-1")
+ bob_store.add_sender(alice_dist)
+
+ msg, alice_rec = encrypt_message(alice_rec, b"hello group")
+ plaintext = decrypt_message(bob_store, msg)
+ assert plaintext == b"hello group"
+
+
+def test_multiple_messages_sequential():
+ """Multiple messages from the same sender decrypt in order."""
+ alice_rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+ store.add_sender(alice_rec.distribution())
+
+ for i in range(5):
+ msg, alice_rec = encrypt_message(alice_rec, f"message {i}".encode())
+ pt = decrypt_message(store, msg)
+ assert pt == f"message {i}".encode()
+
+
+def test_multi_member_group():
+ """Three members: Alice sends, Bob and Carol both decrypt."""
+ alice_rec = SenderKeyRecord.create("alice")
+ alice_dist = alice_rec.distribution()
+
+ bob_store = GroupSenderKeyStore("group-1")
+ bob_store.add_sender(alice_dist)
+
+ carol_store = GroupSenderKeyStore("group-1")
+ carol_store.add_sender(alice_dist)
+
+ msg, alice_rec = encrypt_message(alice_rec, b"broadcast")
+
+ assert decrypt_message(bob_store, msg) == b"broadcast"
+ assert decrypt_message(carol_store, msg) == b"broadcast"
+
+
+def test_bidirectional_chat():
+ """Alice and Bob both send and receive."""
+ alice_rec = SenderKeyRecord.create("alice")
+ bob_rec = SenderKeyRecord.create("bob")
+
+ alice_store = GroupSenderKeyStore("group-1")
+ alice_store.add_sender(bob_rec.distribution())
+
+ bob_store = GroupSenderKeyStore("group-1")
+ bob_store.add_sender(alice_rec.distribution())
+
+ msg1, alice_rec = encrypt_message(alice_rec, b"hi bob")
+ assert decrypt_message(bob_store, msg1) == b"hi bob"
+
+ msg2, bob_rec = encrypt_message(bob_rec, b"hi alice")
+ assert decrypt_message(alice_store, msg2) == b"hi alice"
+
+
+def test_out_of_order_delivery():
+ """Messages delivered out of order are decrypted correctly (up to MAX_SKIP)."""
+ alice_rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+ store.add_sender(alice_rec.distribution())
+
+ msg0, alice_rec = encrypt_message(alice_rec, b"msg 0")
+ msg1, alice_rec = encrypt_message(alice_rec, b"msg 1")
+ msg2, alice_rec = encrypt_message(alice_rec, b"msg 2")
+
+ # Deliver out of order: 2, 0, 1
+ assert decrypt_message(store, msg2) == b"msg 2"
+ assert decrypt_message(store, msg0) == b"msg 0"
+ assert decrypt_message(store, msg1) == b"msg 1"
+
+
+def test_replay_rejected():
+ """A message decrypted twice raises an error (replay protection)."""
+ alice_rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+ store.add_sender(alice_rec.distribution())
+
+ msg, alice_rec = encrypt_message(alice_rec, b"once only")
+ decrypt_message(store, msg)
+
+ with pytest.raises(ValueError, match="already consumed"):
+ decrypt_message(store, msg)
+
+
+def test_unknown_sender_rejected():
+ """Message from an unknown sender raises ValueError."""
+ alice_rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+
+ msg, _ = encrypt_message(alice_rec, b"who am i")
+ with pytest.raises(ValueError, match="Unknown sender"):
+ decrypt_message(store, msg)
+
+
+def test_non_member_cannot_decrypt():
+ """Eve (not in group) cannot decrypt Alice's messages."""
+ alice_rec = SenderKeyRecord.create("alice")
+ eve_store = GroupSenderKeyStore("group-1")
+
+ msg, _ = encrypt_message(alice_rec, b"secret")
+ with pytest.raises(ValueError, match="Unknown sender"):
+ decrypt_message(eve_store, msg)
+
+
+def test_key_rotation_on_member_removal():
+ """After rotation, old chain keys cannot decrypt new messages."""
+ alice_rec = SenderKeyRecord.create("alice")
+ old_dist = alice_rec.distribution()
+
+ # Eve had Alice's old key
+ eve_store = GroupSenderKeyStore("group-1")
+ eve_store.add_sender(old_dist)
+
+ # Alice rotates (member removed from group)
+ alice_rec = alice_rec.rotate()
+ new_dist = alice_rec.distribution()
+
+ # Bob gets the new distribution
+ bob_store = GroupSenderKeyStore("group-1")
+ bob_store.add_sender(new_dist)
+
+ msg, alice_rec = encrypt_message(alice_rec, b"post-rotation")
+ assert decrypt_message(bob_store, msg) == b"post-rotation"
+
+ # Eve cannot decrypt with old key
+ with pytest.raises(Exception):
+ decrypt_message(eve_store, msg)
+
+
+def test_distribution_serialization():
+ """SenderKeyDistribution round-trips through serialize/deserialize."""
+ rec = SenderKeyRecord.create("alice")
+ dist = rec.distribution()
+ data = dist.serialize()
+ recovered = SenderKeyDistribution.deserialize(data)
+
+ assert recovered.sender_id == dist.sender_id
+ assert recovered.chain_key == dist.chain_key
+ assert recovered.iteration == dist.iteration
+ assert recovered.signing_pk == dist.signing_pk
+
+
+def test_message_serialization():
+ """SenderKeyMessage round-trips through serialize/deserialize."""
+ rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+ store.add_sender(rec.distribution())
+
+ msg, _ = encrypt_message(rec, b"serialize me")
+ data = msg.serialize()
+ recovered = SenderKeyMessage.deserialize(data)
+
+ assert recovered.sender_id == msg.sender_id
+ assert recovered.iteration == msg.iteration
+ assert recovered.ciphertext == msg.ciphertext
+ assert recovered.nonce == msg.nonce
+ assert recovered.signature == msg.signature
+
+ # Deserialized message still decrypts
+ pt = decrypt_message(store, recovered)
+ assert pt == b"serialize me"
+
+
+def test_tampered_ciphertext_rejected():
+ """Modifying the ciphertext makes signature verification fail."""
+ alice_rec = SenderKeyRecord.create("alice")
+ store = GroupSenderKeyStore("group-1")
+ store.add_sender(alice_rec.distribution())
+
+ msg, _ = encrypt_message(alice_rec, b"authentic")
+ msg.ciphertext = bytes([b ^ 0xff for b in msg.ciphertext])
+
+ with pytest.raises(Exception):
+ decrypt_message(store, msg)
+
+
+def test_store_sender_count():
+ """GroupSenderKeyStore tracks sender count correctly."""
+ store = GroupSenderKeyStore("group-1")
+ assert store.sender_count == 0
+
+ store.add_sender(SenderKeyRecord.create("alice").distribution())
+ store.add_sender(SenderKeyRecord.create("bob").distribution())
+ assert store.sender_count == 2
+
+ store.remove_sender("alice")
+ assert store.sender_count == 1