From 15f67ce68599749d318c0368cbf21eb5eeeaaa38 Mon Sep 17 00:00:00 2001 From: Bernd Mueller Date: Tue, 21 Apr 2026 17:48:26 +0200 Subject: [PATCH 1/3] Added checks for valid PublicKeys in encryption/decryption functions --- .gitignore | 3 +++ tests/test_base.py | 27 +++++++++++++++++++++++++++ threema/gateway/e2e.py | 2 ++ threema/gateway/key.py | 24 ++++++++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/.gitignore b/.gitignore index 5a893ff..d7a2377 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ gen # MyPy Cache .mypy_cache/ + +# OS specific +.DS_Store diff --git a/tests/test_base.py b/tests/test_base.py index c6b3086..96cecab 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,5 +1,7 @@ import libnacl +import libnacl.public import pytest +from packaging.version import Version from threema.gateway import ( e2e, @@ -25,6 +27,21 @@ def test_incorrect_ciphertext(self): e2e._pk_decrypt(key_pair, nonce, data_encrypted + b'0') assert 'decrypt' in str(exc_info.value) + def test_invalid_non_contributory_public_key(self): + all_zero_public_key = libnacl.public.PublicKey( + bytes(libnacl.crypto_box_PUBLICKEYBYTES)) + key_pair = key.Key.generate_secret_key, all_zero_public_key + data_in = b"meow" + nonce = b"0" * 24 + + with pytest.raises(libnacl.CryptError) as exc_info: + e2e._pk_encrypt(key_pair, data_in, nonce=nonce) + assert "Invalid public key (non-contributory)" in str(exc_info.value) + + with pytest.raises(libnacl.CryptError) as exc_info: + e2e._pk_decrypt(key_pair, nonce, bytes(5)) + assert "Invalid public key (non-contributory)" in str(exc_info.value) + def test_valid(self): key_pair = key.Key.generate_pair() data_in = b'meow' @@ -32,3 +49,13 @@ def test_valid(self): _, data_encrypted = e2e._pk_encrypt(key_pair, data_in, nonce=nonce) data_out = e2e._pk_decrypt(key_pair, nonce, data_encrypted) assert data_in == data_out + + @pytest.mark.skipif( + Version(libnacl.sodium_version_string().decode("ascii")) < Version("1.0.7"), + reason="no zero-result check on X25519 in this libnacl backend") + def test_libsodium_non_contributory_public_key(self): + all_zero_pk = libnacl.public.PublicKey(bytes(libnacl.crypto_box_PUBLICKEYBYTES)) + alice_sk, _ = key.Key.generate_pair() + with pytest.raises(libnacl.CryptError) as exc_info: + libnacl.public.Box(alice_sk, all_zero_pk) + assert 'Unable to compute shared key' in str(exc_info) diff --git a/threema/gateway/e2e.py b/threema/gateway/e2e.py index da041f4..847f523 100644 --- a/threema/gateway/e2e.py +++ b/threema/gateway/e2e.py @@ -82,6 +82,7 @@ def _pk_encrypt(key_pair: Tuple[Key, Key], data: bytes, nonce: Optional[bytes] = """ # Assemble and encrypt the payload private, public = key_pair + Key.ensure_valid_public_key(public) box = libnacl.public.Box(sk=private, pk=public) return box.encrypt(data, nonce=nonce, pack_nonce=False) @@ -103,6 +104,7 @@ def _pk_decrypt(key_pair: Tuple[Key, Key], nonce: bytes, data: bytes): """ # Decrypt payload private, public = key_pair + Key.ensure_valid_public_key(public) box = libnacl.public.Box(sk=private, pk=public) return box.decrypt(data, nonce=nonce) diff --git a/threema/gateway/key.py b/threema/gateway/key.py index fc2a06e..bdeb215 100644 --- a/threema/gateway/key.py +++ b/threema/gateway/key.py @@ -42,6 +42,10 @@ def hash(message, hash_type): return hmac.new(HMAC.keys[hash_type], message.encode('ascii'), hashlib.sha256) +_NON_CONTRIBUTORY_PUBLIC_KEY = libnacl.public.PublicKey( + bytes(libnacl.crypto_box_PUBLICKEYBYTES)) + + class Key: """ Encode or decode a key. @@ -154,3 +158,23 @@ def derive_public(private_key): Return the :class:`libnacl.public.PublicKey` instance. """ return libnacl.public.PublicKey(private_key.pk) + + @staticmethod + def ensure_valid_public_key(public_key): + """ + Ensure a public key is valid. + + Arguments: + - `public_key`: An instance of + :class:`libnacl.public.PublicKey`. + + Raises :class:`libnacl.CryptError` if the public key is invalid. + """ + if not isinstance(public_key, libnacl.public.PublicKey): + raise libnacl.CryptError("Invalid public key") + if len(public_key.pk) != libnacl.crypto_box_PUBLICKEYBYTES: + raise libnacl.CryptError("Invalid public key") + + # Reject all-zero public keys (mon-contributory) + if public_key == _NON_CONTRIBUTORY_PUBLIC_KEY: + raise libnacl.CryptError("Invalid public key (non-contributory)") From 3a1835f597e01ec0c7c0735fc57b6f43db6a18a2 Mon Sep 17 00:00:00 2001 From: Lenny Date: Thu, 23 Apr 2026 18:22:56 +0200 Subject: [PATCH 2/3] Fix some unicode support issues --- threema/gateway/e2e.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/threema/gateway/e2e.py b/threema/gateway/e2e.py index 847f523..7f40658 100644 --- a/threema/gateway/e2e.py +++ b/threema/gateway/e2e.py @@ -246,7 +246,7 @@ def add_callback_route( if receive_handler is None: receive_handler = Message.receive context = CallbackContext( - connection.secret.encode('ascii'), + connection.secret.encode('utf-8'), connection, message_handler, receive_handler, @@ -1288,7 +1288,7 @@ async def pack(self, writer): # Pack payload (compact JSON encoding) try: - content = json.dumps(content, separators=(',', ':')).encode('ascii') + content = json.dumps(content, separators=(',', ':')).encode('utf-8') except UnicodeError as exc: raise MessageError('Could not encode JSON') from exc From a3bea5afd59a2dc055dce8b03a1cc691cb32a899 Mon Sep 17 00:00:00 2001 From: Lenny Date: Thu, 23 Apr 2026 18:26:39 +0200 Subject: [PATCH 3/3] Update Python support list --- .github/workflows/test-manual.yml | 2 +- .github/workflows/test.yml | 2 +- CHANGELOG.rst | 8 ++++++++ setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-manual.yml b/.github/workflows/test-manual.yml index beb7195..789985b 100644 --- a/.github/workflows/test-manual.yml +++ b/.github/workflows/test-manual.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] + python-version: ["3.13", "3.12", "3.11", "3.10"] event-loop: [asyncio, uvloop] steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ed505a..0124f3e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] + python-version: ["3.13", "3.12", "3.11", "3.10"] event-loop: [asyncio, uvloop] steps: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d87d0c..82122ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ********* +Unreleased +---------- + +- Add support for Python 3.13 +- Drop support for Python versions below 3.10 +- Check for non-contributory public keys (Note: When using a modern-ish + libsodium version, this check is redundant) + `8.0.0`_ (2024-08-21) --------------------- diff --git a/setup.cfg b/setup.cfg index d4ca020..49e15d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bdist_wheel] -python-tag = py38.py39.py310.py311.py312 +python-tag = py310.py311.py312.py313 [flake8] max-line-length = 90