diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1ee2b4..12450f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Sections ### Developers --> +## [3.5.2] - 2021-07-22 + +- Switch from ed25519 to pynacl. [#355](https://github.com/ikalchev/HAP-python/pull/355) + ## [3.5.1] - 2021-07-04 # Changed diff --git a/pyhap/const.py b/pyhap/const.py index fd85324a..08bda221 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -1,10 +1,10 @@ """This module contains constants used by other modules.""" MAJOR_VERSION = 3 MINOR_VERSION = 5 -PATCH_VERSION = 1 +PATCH_VERSION = 2 __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 5) +REQUIRED_PYTHON_VER = (3, 6) BASE_UUID = "-0000-1000-8000-0026BB765291" diff --git a/pyhap/encoder.py b/pyhap/encoder.py index b7612c66..9170c3e9 100644 --- a/pyhap/encoder.py +++ b/pyhap/encoder.py @@ -6,7 +6,8 @@ import json import uuid -import ed25519 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 class AccessoryEncoder: @@ -57,8 +58,19 @@ def persist(fp, state): "mac": state.mac, "config_version": state.config_version, "paired_clients": paired_clients, - "private_key": bytes.hex(state.private_key.to_seed()), - "public_key": bytes.hex(state.public_key.to_bytes()), + "private_key": bytes.hex( + state.private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + ), + "public_key": bytes.hex( + state.public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + ), } json.dump(config_state, fp) @@ -75,5 +87,9 @@ def load_into(fp, state): uuid.UUID(client): bytes.fromhex(key) for client, key in loaded["paired_clients"].items() } - state.private_key = ed25519.SigningKey(bytes.fromhex(loaded["private_key"])) - state.public_key = ed25519.VerifyingKey(bytes.fromhex(loaded["public_key"])) + state.private_key = ed25519.Ed25519PrivateKey.from_private_bytes( + bytes.fromhex(loaded["private_key"]) + ) + state.public_key = ed25519.Ed25519PublicKey.from_public_bytes( + bytes.fromhex(loaded["public_key"]) + ) diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index c0b06eea..0c90bde9 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -9,18 +9,19 @@ from urllib.parse import parse_qs, urlparse import uuid -from cryptography.exceptions import InvalidTag +from cryptography.exceptions import InvalidSignature, InvalidTag +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -import curve25519 -import ed25519 +from pyhap import tlv from pyhap.const import ( CATEGORY_BRIDGE, HAP_REPR_CHARS, HAP_REPR_STATUS, HAP_SERVER_STATUS, ) -from pyhap import tlv from pyhap.util import long_to_bytes from .hap_crypto import hap_hkdf, pad_tls_nonce @@ -368,11 +369,11 @@ def _pairing_four(self, client_username, client_ltpk, client_proof, encryption_k ) data = output_key + client_username + client_ltpk - verifying_key = ed25519.VerifyingKey(client_ltpk) + verifying_key = ed25519.Ed25519PublicKey.from_public_bytes(client_ltpk) try: verifying_key.verify(client_proof, data) - except ed25519.BadSignatureError: + except InvalidSignature: logger.error("Bad signature, abort.") raise @@ -390,7 +391,10 @@ def _pairing_five(self, client_username, client_ltpk, encryption_key): long_to_bytes(session_key), self.PAIRING_5_SALT, self.PAIRING_5_INFO ) - server_public = self.state.public_key.to_bytes() + server_public = self.state.public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) mac = self.state.mac.encode() material = output_key + mac + server_public @@ -457,16 +461,21 @@ def _pair_verify_one(self, tlv_objects): logger.debug("%s: Pair verify [1/2].", self.client_address) client_public = tlv_objects[HAP_TLV_TAGS.PUBLIC_KEY] - private_key = curve25519.Private() - public_key = private_key.get_public() - shared_key = private_key.get_shared_key( - curve25519.Public(client_public), - # Key is hashed before being returned, we don't want it; This fixes that. - lambda x: x, + private_key = x25519.X25519PrivateKey.generate() + public_key = private_key.public_key() + shared_key = private_key.exchange( + x25519.X25519PublicKey.from_public_bytes(client_public) ) mac = self.state.mac.encode() - material = public_key.serialize() + mac + client_public + material = ( + public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + + mac + + client_public + ) server_proof = self.state.private_key.sign(material) output_key = hap_hkdf(shared_key, self.PVERIFY_1_SALT, self.PVERIFY_1_INFO) @@ -487,7 +496,10 @@ def _pair_verify_one(self, tlv_objects): HAP_TLV_TAGS.ENCRYPTED_DATA, aead_message, HAP_TLV_TAGS.PUBLIC_KEY, - public_key.serialize(), + public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ), ) self._send_tlv_pairing_response(data) @@ -513,7 +525,10 @@ def _pair_verify_two(self, tlv_objects): material = ( self.enc_context["client_public"] + client_username - + self.enc_context["public_key"].serialize() + + self.enc_context["public_key"].public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) ) client_uuid = uuid.UUID(str(client_username, "utf-8")) @@ -528,10 +543,10 @@ def _pair_verify_two(self, tlv_objects): self._send_authentication_error_tlv_response(HAP_TLV_STATES.M4) return - verifying_key = ed25519.VerifyingKey(perm_client_public) + verifying_key = ed25519.Ed25519PublicKey.from_public_bytes(perm_client_public) try: verifying_key.verify(dec_tlv_objects[HAP_TLV_TAGS.PROOF], material) - except ed25519.BadSignatureError: + except InvalidSignature: logger.error("%s: Bad signature, abort.", self.client_address) self._send_authentication_error_tlv_response(HAP_TLV_STATES.M4) return diff --git a/pyhap/state.py b/pyhap/state.py index 3fc2a2f7..0c279601 100644 --- a/pyhap/state.py +++ b/pyhap/state.py @@ -1,5 +1,5 @@ """Module for `State` class.""" -import ed25519 +from cryptography.hazmat.primitives.asymmetric import ed25519 from pyhap import util from pyhap.const import DEFAULT_CONFIG_VERSION, DEFAULT_PORT @@ -11,8 +11,7 @@ class State: That includes all needed for setup of driver and pairing. """ - def __init__(self, *, address=None, mac=None, - pincode=None, port=None): + def __init__(self, *, address=None, mac=None, pincode=None, port=None): """Initialize a new object. Create key pair. Must be called with keyword arguments. @@ -26,9 +25,8 @@ def __init__(self, *, address=None, mac=None, self.config_version = DEFAULT_CONFIG_VERSION self.paired_clients = {} - sk, vk = ed25519.create_keypair() - self.private_key = sk - self.public_key = vk + self.private_key = ed25519.Ed25519PrivateKey.generate() + self.public_key = self.private_key.public_key() # ### Pairing ### @property diff --git a/requirements.txt b/requirements.txt index 92ff6d05..5b6a15d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ h11 -curve25519-donna -ed25519 cryptography zeroconf diff --git a/requirements_all.txt b/requirements_all.txt index 9b63a4c1..41563ce4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,6 +1,4 @@ base36 -curve25519-donna -ed25519 cryptography pyqrcode h11 diff --git a/setup.py b/setup.py index 44cabe7d..c67ffbe0 100644 --- a/setup.py +++ b/setup.py @@ -3,33 +3,26 @@ import pyhap.const as pyhap_const - -NAME = 'HAP-python' -DESCRIPTION = 'HomeKit Accessory Protocol implementation in python' -URL = 'https://github.com/ikalchev/{}'.format(NAME) -AUTHOR = 'Ivan Kalchev' +NAME = "HAP-python" +DESCRIPTION = "HomeKit Accessory Protocol implementation in python" +URL = "https://github.com/ikalchev/{}".format(NAME) +AUTHOR = "Ivan Kalchev" PROJECT_URLS = { - 'Bug Reports': '{}/issues'.format(URL), - 'Documentation': 'http://hap-python.readthedocs.io/en/latest/', - 'Source': '{}/tree/master'.format(URL), + "Bug Reports": "{}/issues".format(URL), + "Documentation": "http://hap-python.readthedocs.io/en/latest/", + "Source": "{}/tree/master".format(URL), } -MIN_PY_VERSION = '.'.join(map(str, pyhap_const.REQUIRED_PYTHON_VER)) +MIN_PY_VERSION = ".".join(map(str, pyhap_const.REQUIRED_PYTHON_VER)) -with open('README.md', 'r', encoding='utf-8') as f: +with open("README.md", "r", encoding="utf-8") as f: README = f.read() -REQUIRES = [ - 'curve25519-donna', - 'ed25519', - 'cryptography', - 'zeroconf>=0.32.0', - 'h11' -] +REQUIRES = ["cryptography", "zeroconf>=0.32.0", "h11"] setup( @@ -37,32 +30,32 @@ version=pyhap_const.__version__, description=DESCRIPTION, long_description=README, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", url=URL, - packages=['pyhap'], + packages=["pyhap"], include_package_data=True, project_urls=PROJECT_URLS, - python_requires='>={}'.format(MIN_PY_VERSION), + python_requires=">={}".format(MIN_PY_VERSION), install_requires=REQUIRES, - license='Apache License 2.0', - license_file='LICENSE', + license="Apache License 2.0", + license_file="LICENSE", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: Apache Software License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Home Automation', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Home Automation", + "Topic :: Software Development :: Libraries :: Python Modules", ], extras_require={ - 'QRCode': ['base36', 'pyqrcode'], - } + "QRCode": ["base36", "pyqrcode"], + }, ) diff --git a/tests/test_accessory_driver.py b/tests/test_accessory_driver.py index 01163c7c..dc1db1de 100644 --- a/tests/test_accessory_driver.py +++ b/tests/test_accessory_driver.py @@ -4,9 +4,10 @@ import tempfile from unittest.mock import MagicMock, patch from uuid import uuid1 -from zeroconf import InterfaceChoice +from cryptography.hazmat.primitives import serialization import pytest +from zeroconf import InterfaceChoice from pyhap import util from pyhap.accessory import STANDALONE_AID, Accessory, Bridge @@ -100,7 +101,13 @@ def test_persist_load(async_zeroconf): # the new accessory. driver = AccessoryDriver(port=51234, persist_file=file.name) driver.load() - assert driver.state.public_key == pk + assert driver.state.public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) == pk.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) def test_persist_cannot_write(async_zeroconf): diff --git a/tests/test_encoder.py b/tests/test_encoder.py index fdd79f71..ca6d0c4a 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -2,7 +2,8 @@ import tempfile import uuid -import ed25519 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 from pyhap import encoder from pyhap.state import State @@ -14,9 +15,16 @@ def test_persist_and_load(): Accessory. Tests if the two accessories have the same property values. """ mac = generate_mac() - _pk, sample_client_pk = ed25519.create_keypair() + _pk = ed25519.Ed25519PrivateKey.generate() + sample_client_pk = _pk.public_key() state = State(mac=mac) - state.add_paired_client(uuid.uuid1(), sample_client_pk.to_bytes()) + state.add_paired_client( + uuid.uuid1(), + sample_client_pk.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ), + ) config_loaded = State() config_loaded.config_version += 2 # change the default state. @@ -27,7 +35,21 @@ def test_persist_and_load(): enc.load_into(fp, config_loaded) assert state.mac == config_loaded.mac - assert state.private_key == config_loaded.private_key - assert state.public_key == config_loaded.public_key + assert state.private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) == config_loaded.private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + assert state.public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) == config_loaded.public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) assert state.config_version == config_loaded.config_version assert state.paired_clients == config_loaded.paired_clients diff --git a/tests/test_state.py b/tests/test_state.py index 5992d249..cb117ae1 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,6 +1,7 @@ """Test for pyhap.state.""" from unittest.mock import patch +from cryptography.hazmat.primitives.asymmetric import ed25519 import pytest from pyhap.state import State @@ -16,12 +17,15 @@ def test_setup(): pin = b"123-45-678" port = 11111 + private_key = ed25519.Ed25519PrivateKey.generate() + with patch("pyhap.util.get_local_address") as mock_local_addr, patch( "pyhap.util.generate_mac" ) as mock_gen_mac, patch("pyhap.util.generate_pincode") as mock_gen_pincode, patch( "pyhap.util.generate_setup_id" ) as mock_gen_setup_id, patch( - "ed25519.create_keypair", return_value=(1, 2) + "cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate", + return_value=private_key, ) as mock_create_keypair: state = State(address=addr, mac=mac, pincode=pin, port=port) @@ -48,9 +52,7 @@ def test_pairing(): """Test if pairing methods work.""" with patch("pyhap.util.get_local_address"), patch("pyhap.util.generate_mac"), patch( "pyhap.util.generate_pincode" - ), patch("pyhap.util.generate_setup_id"), patch( - "ed25519.create_keypair", return_value=(1, 2) - ): + ), patch("pyhap.util.generate_setup_id"): state = State() assert not state.paired