From b88bcc6961a9eada664b7c19ef1526eefbfa7e23 Mon Sep 17 00:00:00 2001 From: Paul Dee Date: Thu, 15 Jul 2021 21:52:29 +0200 Subject: [PATCH] Fix non-transient pairing Hitherto, a subtle bug existed in the HAP module. During various rounds of verification, signing was done by a temporally generated key, instead of the 'accessory' long term (LT) key. Accessory is the ap2-receiver. We now generate an Ed25519 key at startup and put it into the "pk" TXT record which makes the LTK available to other devices via the out-of-band channel, for long term access. More info: https://developer.apple.com/homekit/specification/ This means that non-transient pairing now works :) To test, add -ftxor 48 (disable Ft48TransientPairing), e.g.: python3 ap2-receiver.py -m myap2 -n en0 -ftxor 48 --- README.md | 1 + ap2-receiver.py | 34 +++++++++++++++++++++++++++++++--- ap2/pairing/hap.py | 22 +++++++++++----------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d3ee689..005ffc8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Very quick python implementation of AP2 protocol using **minimal multi-room** features. For now it implements: - HomeKit transient pairing (SRP/Curve25519/ChaCha20-Poly1305) +- HomeKit non-transient pairing - FairPlay (v3) authentication - Receiving of both REALTIME and BUFFERED Airplay2 audio streams - Airplay2 Service publication diff --git a/ap2-receiver.py b/ap2-receiver.py index b80118e..c9a5ead 100644 --- a/ap2-receiver.py +++ b/ap2-receiver.py @@ -153,6 +153,33 @@ class Feat(IntFlag): | Feat.Ft14MFiSoftware | Feat.Ft09AirPlayAudio ) + +class LTPK(): + # Note that this must be Ed25519 (and not Curve25519 i.e. X25519) for + # Non Transient Pairing to work. (i.e. disable Ft48TransientPairing) + def __init__(self): + from cryptography.hazmat.primitives.asymmetric import ed25519 + from cryptography.hazmat.primitives import serialization + self.accessory_curve = ed25519.Ed25519PrivateKey.generate() + self.accessory_curve_public = self.accessory_curve.public_key( + ).public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + self.public_int = int.from_bytes(self.accessory_curve_public, byteorder='big') + self.public_string = str.lower("{0:0>4X}".format(self.public_int)) + + def get_pub_string(self): + print('Got pub bytes:', self.public_string) + return self.public_string + + def get_pub_bytes(self): + return self.accessory_curve_public + + def get_private_bytes(self): + return self.accessory_curve + + DEVICE_ID = None IPV4 = None IPV6 = None @@ -163,6 +190,7 @@ class Feat(IntFlag): HTTP_CT_PARAM = "text/parameters" HTTP_CT_IMAGE = "image/jpeg" HTTP_CT_DMAP = "application/x-dmap-tagged" +LTPK = LTPK() def setup_global_structs(args): @@ -263,7 +291,7 @@ def setup_global_structs(args): "gid": "5dccfd20-b166-49cc-a593-6abd5f724ddb", # UUID generated casually "gcgl": "0", # "vn": "65537", - "pk": "de352b0df39042e201d31564049023af58a106c6d904b74a68aa65012852997f" + "pk": LTPK.get_pub_string() } @@ -703,7 +731,7 @@ def handle_pair_setup(self): hexdump(body) if not self.server.hap: - self.server.hap = Hap() + self.server.hap = Hap(LTPK.get_private_bytes(), LTPK.get_pub_bytes()) res = self.server.hap.pair_setup(body) self.send_response(200) @@ -724,7 +752,7 @@ def handle_pair_verify(self): body = self.rfile.read(content_len) if not self.server.hap: - self.server.hap = Hap() + self.server.hap = Hap(LTPK.get_private_bytes(), LTPK.get_pub_bytes()) res = self.server.hap.pair_verify(body) self.send_response(200) diff --git a/ap2/pairing/hap.py b/ap2/pairing/hap.py index 6df2e28..3a3a5a8 100644 --- a/ap2/pairing/hap.py +++ b/ap2/pairing/hap.py @@ -7,6 +7,8 @@ import hkdf from cryptography.hazmat.primitives.asymmetric import x25519 +# We build the Ed25519 in ap2-receiver, and pass it to HAP() +# from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives import serialization import nacl.signing from Crypto.Cipher import ChaCha20_Poly1305 @@ -104,14 +106,14 @@ def encode(req): class Hap: - def __init__(self): + def __init__(self, ltsk, ltpk): self.transient = False self.encrypted = False self.pair_setup_steps_n = 5 self.accessory_id = b"00000000-0000-0000-0000-deadbeef0bad" - # self.accessory_ltsk = 0 - # self.accessory_ltpk = 0 + self.accessory_ltsk = ltsk + self.accessory_ltpk = ltpk def request(self, req): req = Tlv8.decode(req) @@ -216,14 +218,12 @@ def pair_setup_m5_m6_3(self, session_key): prk = hkdf.hkdf_extract(b"Pair-Setup-Accessory-Sign-Salt", self.ctx.session_key) accessory_x = hkdf.hkdf_expand(prk, b"Pair-Setup-Accessory-Sign-Info", 32) - self.accessory_ltsk = nacl.signing.SigningKey.generate() - self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key) - - self.accessory_id = b"00000000-0000-0000-0000-f0989d7cbbab" + # self.accessory_ltsk = nacl.signing.SigningKey.generate() + # self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key) accessory_info = accessory_x + self.accessory_id + self.accessory_ltpk accessory_signed = self.accessory_ltsk.sign(accessory_info) - accessory_sig = accessory_signed.signature + accessory_sig = accessory_signed # .signature dec_tlv = Tlv8.encode([ Tlv8.Tag.IDENTIFIER, self.accessory_id, @@ -246,12 +246,12 @@ def pair_verify_m1_m2(self, client_public): ) self.accessory_shared_key = self.accessory_curve.exchange(x25519.X25519PublicKey.from_public_bytes(client_public)) - self.accessory_ltsk = nacl.signing.SigningKey.generate() - self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key) + # self.accessory_ltsk = nacl.signing.SigningKey.generate() + # self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key) accessory_info = self.accessory_curve_public + self.accessory_id + client_public accessory_signed = self.accessory_ltsk.sign(accessory_info) - accessory_sig = accessory_signed.signature + accessory_sig = accessory_signed # .signature sub_tlv = Tlv8.encode([ Tlv8.Tag.IDENTIFIER, self.accessory_id,