Skip to content

Commit b88bcc6

Browse files
committed
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
1 parent 57e4a3b commit b88bcc6

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Very quick python implementation of AP2 protocol using **minimal
44
multi-room** features. For now it implements:
55
- HomeKit transient pairing (SRP/Curve25519/ChaCha20-Poly1305)
6+
- HomeKit non-transient pairing
67
- FairPlay (v3) authentication
78
- Receiving of both REALTIME and BUFFERED Airplay2 audio streams
89
- Airplay2 Service publication

ap2-receiver.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,33 @@ class Feat(IntFlag):
153153
| Feat.Ft14MFiSoftware | Feat.Ft09AirPlayAudio
154154
)
155155

156+
157+
class LTPK():
158+
# Note that this must be Ed25519 (and not Curve25519 i.e. X25519) for
159+
# Non Transient Pairing to work. (i.e. disable Ft48TransientPairing)
160+
def __init__(self):
161+
from cryptography.hazmat.primitives.asymmetric import ed25519
162+
from cryptography.hazmat.primitives import serialization
163+
self.accessory_curve = ed25519.Ed25519PrivateKey.generate()
164+
self.accessory_curve_public = self.accessory_curve.public_key(
165+
).public_bytes(
166+
encoding=serialization.Encoding.Raw,
167+
format=serialization.PublicFormat.Raw
168+
)
169+
self.public_int = int.from_bytes(self.accessory_curve_public, byteorder='big')
170+
self.public_string = str.lower("{0:0>4X}".format(self.public_int))
171+
172+
def get_pub_string(self):
173+
print('Got pub bytes:', self.public_string)
174+
return self.public_string
175+
176+
def get_pub_bytes(self):
177+
return self.accessory_curve_public
178+
179+
def get_private_bytes(self):
180+
return self.accessory_curve
181+
182+
156183
DEVICE_ID = None
157184
IPV4 = None
158185
IPV6 = None
@@ -163,6 +190,7 @@ class Feat(IntFlag):
163190
HTTP_CT_PARAM = "text/parameters"
164191
HTTP_CT_IMAGE = "image/jpeg"
165192
HTTP_CT_DMAP = "application/x-dmap-tagged"
193+
LTPK = LTPK()
166194

167195

168196
def setup_global_structs(args):
@@ -263,7 +291,7 @@ def setup_global_structs(args):
263291
"gid": "5dccfd20-b166-49cc-a593-6abd5f724ddb", # UUID generated casually
264292
"gcgl": "0",
265293
# "vn": "65537",
266-
"pk": "de352b0df39042e201d31564049023af58a106c6d904b74a68aa65012852997f"
294+
"pk": LTPK.get_pub_string()
267295
}
268296

269297

@@ -703,7 +731,7 @@ def handle_pair_setup(self):
703731
hexdump(body)
704732

705733
if not self.server.hap:
706-
self.server.hap = Hap()
734+
self.server.hap = Hap(LTPK.get_private_bytes(), LTPK.get_pub_bytes())
707735
res = self.server.hap.pair_setup(body)
708736

709737
self.send_response(200)
@@ -724,7 +752,7 @@ def handle_pair_verify(self):
724752
body = self.rfile.read(content_len)
725753

726754
if not self.server.hap:
727-
self.server.hap = Hap()
755+
self.server.hap = Hap(LTPK.get_private_bytes(), LTPK.get_pub_bytes())
728756
res = self.server.hap.pair_verify(body)
729757

730758
self.send_response(200)

ap2/pairing/hap.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import hkdf
99
from cryptography.hazmat.primitives.asymmetric import x25519
10+
# We build the Ed25519 in ap2-receiver, and pass it to HAP()
11+
# from cryptography.hazmat.primitives.asymmetric import ed25519
1012
from cryptography.hazmat.primitives import serialization
1113
import nacl.signing
1214
from Crypto.Cipher import ChaCha20_Poly1305
@@ -104,14 +106,14 @@ def encode(req):
104106

105107

106108
class Hap:
107-
def __init__(self):
109+
def __init__(self, ltsk, ltpk):
108110
self.transient = False
109111
self.encrypted = False
110112
self.pair_setup_steps_n = 5
111113

112114
self.accessory_id = b"00000000-0000-0000-0000-deadbeef0bad"
113-
# self.accessory_ltsk = 0
114-
# self.accessory_ltpk = 0
115+
self.accessory_ltsk = ltsk
116+
self.accessory_ltpk = ltpk
115117

116118
def request(self, req):
117119
req = Tlv8.decode(req)
@@ -216,14 +218,12 @@ def pair_setup_m5_m6_3(self, session_key):
216218
prk = hkdf.hkdf_extract(b"Pair-Setup-Accessory-Sign-Salt", self.ctx.session_key)
217219
accessory_x = hkdf.hkdf_expand(prk, b"Pair-Setup-Accessory-Sign-Info", 32)
218220

219-
self.accessory_ltsk = nacl.signing.SigningKey.generate()
220-
self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key)
221-
222-
self.accessory_id = b"00000000-0000-0000-0000-f0989d7cbbab"
221+
# self.accessory_ltsk = nacl.signing.SigningKey.generate()
222+
# self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key)
223223

224224
accessory_info = accessory_x + self.accessory_id + self.accessory_ltpk
225225
accessory_signed = self.accessory_ltsk.sign(accessory_info)
226-
accessory_sig = accessory_signed.signature
226+
accessory_sig = accessory_signed # .signature
227227

228228
dec_tlv = Tlv8.encode([
229229
Tlv8.Tag.IDENTIFIER, self.accessory_id,
@@ -246,12 +246,12 @@ def pair_verify_m1_m2(self, client_public):
246246
)
247247
self.accessory_shared_key = self.accessory_curve.exchange(x25519.X25519PublicKey.from_public_bytes(client_public))
248248

249-
self.accessory_ltsk = nacl.signing.SigningKey.generate()
250-
self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key)
249+
# self.accessory_ltsk = nacl.signing.SigningKey.generate()
250+
# self.accessory_ltpk = bytes(self.accessory_ltsk.verify_key)
251251

252252
accessory_info = self.accessory_curve_public + self.accessory_id + client_public
253253
accessory_signed = self.accessory_ltsk.sign(accessory_info)
254-
accessory_sig = accessory_signed.signature
254+
accessory_sig = accessory_signed # .signature
255255

256256
sub_tlv = Tlv8.encode([
257257
Tlv8.Tag.IDENTIFIER, self.accessory_id,

0 commit comments

Comments
 (0)