Skip to content

Commit

Permalink
Fix non-transient pairing
Browse files Browse the repository at this point in the history
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
  • Loading branch information
systemcrash committed Jul 15, 2021
1 parent 57e4a3b commit b88bcc6
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 31 additions & 3 deletions ap2-receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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()
}


Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
22 changes: 11 additions & 11 deletions ap2/pairing/hap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit b88bcc6

Please sign in to comment.