Skip to content

Commit e544a88

Browse files
committed
Improve M2/M3 generation
1 parent 9d0f9ed commit e544a88

7 files changed

Lines changed: 640 additions & 191 deletions

File tree

internal/airplay/fairplay.go

Lines changed: 26 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ import (
99
"fmt"
1010
)
1111

12-
// fairPlayM1 is the fixed first message in the FairPlay SAP exchange.
13-
var fairPlayM1 = mustDecodeHexFP("46504c590301010000000004020003bb")
14-
1512
var ErrFairPlayUnsupported = errors.New("receiver does not support FairPlay SAP")
1613

1714
func mustDecodeHexFP(s string) []byte {
@@ -30,9 +27,16 @@ func (c *AirPlayClient) FairPlaySetup(ctx context.Context) error {
3027

3128
dbg("[FP] starting FairPlay SAP handshake...")
3229

30+
// Apple's sender creates one opaque FPSAP context before m1 and retains it
31+
// for m3 and encrypted-key creation. Keep the equivalent state together for
32+
// the lifetime of this authentication attempt.
33+
fpsap, err := newFPSAPSession(rand.Reader)
34+
if err != nil {
35+
return fmt.Errorf("initialize FPSAP session: %w", err)
36+
}
37+
3338
// Phase 1: Send m1, receive m2
34-
m1 := make([]byte, len(fairPlayM1))
35-
copy(m1, fairPlayM1)
39+
m1 := fpsap.message1()
3640

3741
dbg("[FP] posting m1 (%d bytes) to /fp-setup", len(m1))
3842
m2, err := c.httpRequest("POST", "/fp-setup", "application/octet-stream", m1,
@@ -45,14 +49,11 @@ func (c *AirPlayClient) FairPlaySetup(ctx context.Context) error {
4549
return fmt.Errorf("fp-setup phase 1 (m1): %w", err)
4650
}
4751

48-
if len(m2) < 12 {
49-
return fmt.Errorf("m2 response too short: %d bytes", len(m2))
50-
}
5152
dbg("[FP] received m2 (%d bytes)", len(m2))
5253
dbg("[FP] m2 first 32: %02x", m2[:min(32, len(m2))])
5354

5455
// Phase 2: Compute m3 and send it to the receiver.
55-
m3, err := fpsapExchangeM3(m2)
56+
m3, err := fpsap.exchangeM3(m2)
5657
if err != nil {
5758
return fmt.Errorf("FPSAPExchange: %w", err)
5859
}
@@ -70,13 +71,11 @@ func (c *AirPlayClient) FairPlaySetup(ctx context.Context) error {
7071
return fmt.Errorf("fp-setup phase 2 (m3): %w", err)
7172
}
7273

73-
if len(m4) < 12 {
74-
return fmt.Errorf("m4 response too short: %d bytes", len(m4))
74+
if err := fpsap.confirmM4(m4); err != nil {
75+
return fmt.Errorf("FPSAP m4: %w", err)
7576
}
7677
dbg("[FP] received m4 (%d bytes)", len(m4))
77-
78-
m4Payload := fplyUnwrap(m4)
79-
dbg("[FP] m4 payload (%d bytes): %02x", len(m4Payload), m4Payload)
78+
dbg("[FP] m4 payload (%d bytes): %02x", len(m4)-12, m4[12:])
8079

8180
// Generate and store IV for stream encryption
8281
var iv [16]byte
@@ -89,17 +88,24 @@ func (c *AirPlayClient) FairPlaySetup(ctx context.Context) error {
8988
c.fpM3 = make([]byte, len(m3))
9089
copy(c.fpM3, m3)
9190

92-
// Build ekey and derive audio encryption key.
93-
// Both sender and receiver call unwrapFairPlayKey(m3, ekey) with the same
94-
// inputs (m3 sent during FP handshake, ekey sent in SETUP body).
95-
ekey := buildEkey()
91+
// Generate the raw audio key and wrap it in the FairPlay ekey record. Apple's
92+
// sender API accepts the raw key as input; the receiver performs the inverse
93+
// operation using the state established by m3.
94+
var fpAesKey [16]byte
95+
if _, err := rand.Read(fpAesKey[:]); err != nil {
96+
return fmt.Errorf("generate FairPlay audio key: %w", err)
97+
}
98+
ekey, err := fpsap.wrapKey(fpAesKey, rand.Reader)
99+
if err != nil {
100+
return fmt.Errorf("wrap FairPlay audio key: %w", err)
101+
}
96102
c.FpEkey = ekey[:]
97103
dbg("[FP] ekey chunk1 [16:32]: %02x", ekey[16:32])
104+
dbg("[FP] ekey key length [32:36]: %d", 16)
98105
dbg("[FP] ekey chunk2 [56:72]: %02x", ekey[56:72])
99106

100-
fpAesKey := unwrapFairPlayKey(c.fpM3, ekey[:])
101107
c.fpAesKey = fpAesKey[:]
102-
dbg("[FP] unwrapFairPlayKey fpAesKey: %02x", fpAesKey[:])
108+
dbg("[FP] wrapped fpAesKey: %02x", fpAesKey[:])
103109
dbg("[FP] m3 first 32 bytes: %02x", c.fpM3[:min(32, len(c.fpM3))])
104110

105111
// Hash with pair-verify shared secret (ECDH X25519) if available.
@@ -125,46 +131,6 @@ func (c *AirPlayClient) FairPlaySetup(ctx context.Context) error {
125131
return nil
126132
}
127133

128-
// buildEkey constructs a 72-byte ekey with the FPLY header format.
129-
// The chunk data is randomized per session so that unwrapFairPlayKey produces
130-
// a unique AES key for each session. Both sender and receiver compute the
131-
// same key from the same (m3, ekey) inputs.
132-
//
133-
// Format (72 bytes):
134-
//
135-
// [0:4] "FPLY"
136-
// [4:8] 01 02 01 00
137-
// [8:12] 00 00 00 3c (0x3c = 60 = remaining bytes)
138-
// [12:16] 00 00 00 00 (padding)
139-
// [16:32] chunk1 (16 bytes, random)
140-
// [32:56] padding (24 bytes, zeros)
141-
// [56:72] chunk2 (16 bytes, random)
142-
func buildEkey() [72]byte {
143-
var ekey [72]byte
144-
copy(ekey[0:4], []byte("FPLY"))
145-
ekey[4] = 0x01
146-
ekey[5] = 0x02
147-
ekey[6] = 0x01
148-
ekey[7] = 0x00
149-
ekey[8] = 0x00
150-
ekey[9] = 0x00
151-
ekey[10] = 0x00
152-
ekey[11] = 0x3c
153-
// Fill chunk1 [16:32] and chunk2 [56:72] with random data
154-
rand.Read(ekey[16:32])
155-
rand.Read(ekey[56:72])
156-
return ekey
157-
}
158-
159-
// fplyUnwrap strips the FPLY framing header and returns the payload.
160-
// If the data doesn't have FPLY framing, it's returned as-is.
161-
func fplyUnwrap(data []byte) []byte {
162-
if len(data) >= 12 && string(data[:4]) == "FPLY" {
163-
return data[12:]
164-
}
165-
return data
166-
}
167-
168134
// deriveStreamKeys derives AES stream encryption keys from the pair-verify shared secret.
169135
func (c *AirPlayClient) deriveStreamKeys() error {
170136
if c.encWriteKey == nil {

internal/airplay/fairplay_crypto.go

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package airplay
22

33
import (
44
"crypto/aes"
5+
"crypto/hmac"
6+
"crypto/sha1"
57
"encoding/binary"
8+
"fmt"
9+
"io"
610
)
711

812
var fairplayInitialSessionKey = [16]byte{
@@ -20,19 +24,7 @@ var fairplayKDFSuffix = [17]byte{
2024
0x97, 0xae, 0x70, 0xfb, 0xe0, 0x00, 0x3f, 0x1c, 0x39,
2125
}
2226

23-
// Only the second 128 bytes of the fixed SAP record participate in the KDF.
24-
var fairplayDefaultSAPTail = [128]byte{
25-
0x00, 0x01, 0xcc, 0x34, 0x2a, 0x5e, 0x5b, 0x1a, 0x67, 0x73, 0xc2, 0x0e, 0x21, 0xb8, 0x22, 0x4d,
26-
0xf8, 0x62, 0x48, 0x18, 0x64, 0xef, 0x81, 0x0a, 0xae, 0x2e, 0x37, 0x03, 0xc8, 0x81, 0x9c, 0x23,
27-
0x53, 0x9d, 0xe5, 0xf5, 0xd7, 0x49, 0xbc, 0x5b, 0x7a, 0x26, 0x6c, 0x49, 0x62, 0x83, 0xce, 0x7f,
28-
0x03, 0x93, 0x7a, 0xe1, 0xf6, 0x16, 0xde, 0x0c, 0x15, 0xff, 0x33, 0x8c, 0xca, 0xff, 0xb0, 0x9e,
29-
0xaa, 0xbb, 0xe4, 0x0f, 0x5d, 0x5f, 0x55, 0x8f, 0xb9, 0x7f, 0x17, 0x31, 0xf8, 0xf7, 0xda, 0x60,
30-
0xa0, 0xec, 0x65, 0x79, 0xc3, 0x3e, 0xa9, 0x83, 0x12, 0xc3, 0xb6, 0x71, 0x35, 0xa6, 0x69, 0x4f,
31-
0xf8, 0x23, 0x05, 0xd9, 0xba, 0x5c, 0x61, 0x5f, 0xa2, 0x54, 0xd2, 0xb1, 0x83, 0x45, 0x83, 0xce,
32-
0xe4, 0x2d, 0x44, 0x26, 0xc8, 0x35, 0xa7, 0xa5, 0xf6, 0xc8, 0x42, 0x1c, 0x0d, 0xa3, 0xf1, 0xc7,
33-
}
34-
35-
func deriveFairPlayWrappingKey(sapTail []byte, message []byte) [16]byte {
27+
func deriveFairPlayWrappingKey(receiverSAP [128]byte, message []byte) [16]byte {
3628
var decrypted [128]byte
3729
decryptFairPlayMessage(message, decrypted[:])
3830

@@ -42,7 +34,7 @@ func deriveFairPlayWrappingKey(sapTail []byte, message []byte) [16]byte {
4234
var material [320]byte
4335
offset := copy(material[:], fairplayKDFPrefix[:])
4436
offset += copy(material[offset:], decrypted[:])
45-
offset += copy(material[offset:], sapTail[:128])
37+
offset += copy(material[offset:], receiverSAP[:])
4638
offset += copy(material[offset:], fairplayKDFSuffix[:])
4739
material[offset] = 0x80
4840
binary.LittleEndian.PutUint64(material[len(material)-8:], uint64(offset)*8)
@@ -59,17 +51,54 @@ func deriveFairPlayWrappingKey(sapTail []byte, message []byte) [16]byte {
5951
return fairplayWordsBigEndian(state)
6052
}
6153

62-
func unwrapFairPlayKey(m3 []byte, ekey []byte) [16]byte {
63-
aesKey := deriveFairPlayWrappingKey(fairplayDefaultSAPTail[:], m3)
64-
cipher, err := aes.NewCipher(aesKey[:])
65-
if err != nil {
66-
panic(err) // aesKey always has the fixed AES-128 length.
54+
// wrapFairPlayKey emits the 72-byte AirPlay v3 record produced by Apple's
55+
// FairPlay sender:
56+
//
57+
// [0:16] FPLY encrypted-key header
58+
// [16:32] per-key random mask
59+
// [32:36] big-endian raw-key length (16)
60+
// [36:56] HMAC-SHA1(session MAC key, record[0:36] || raw key)
61+
// [56:72] AES-wrapped (raw key XOR mask)
62+
//
63+
// Both session keys depend on the receiver's decrypted m2 SAP. Reusing a
64+
// captured receiver SAP makes the record self-consistent only for that capture.
65+
// The native sender obtains the mask from its session PRNG; accepting an entropy
66+
// source here preserves the wire semantics without reproducing that PRNG.
67+
func wrapFairPlayKey(receiverSAP [128]byte, m3 []byte, rawKey [16]byte, entropy io.Reader) ([72]byte, error) {
68+
var ekey [72]byte
69+
if err := validateFPSAPRecord(m3, 3, 152); err != nil {
70+
return ekey, fmt.Errorf("invalid m3: %w", err)
71+
}
72+
if mode := m3[12]; int(mode) >= len(fairplayMessageIV) {
73+
return ekey, fmt.Errorf("unsupported FairPlay mode %d", mode)
6774
}
6875

69-
var keyOut [16]byte
70-
cipher.Decrypt(keyOut[:], ekey[56:72])
71-
for i := range keyOut {
72-
keyOut[i] ^= ekey[16+i]
76+
copy(ekey[:], []byte{
77+
'F', 'P', 'L', 'Y', 0x01, 0x02, 0x01, 0x00,
78+
0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00,
79+
})
80+
if _, err := io.ReadFull(entropy, ekey[16:32]); err != nil {
81+
return [72]byte{}, fmt.Errorf("generate FairPlay key mask: %w", err)
7382
}
74-
return keyOut
83+
binary.BigEndian.PutUint32(ekey[32:36], uint32(len(rawKey)))
84+
85+
wrappingKey := deriveFairPlayWrappingKey(receiverSAP, m3)
86+
cipher, err := aes.NewCipher(wrappingKey[:])
87+
if err != nil {
88+
return [72]byte{}, fmt.Errorf("create FairPlay wrapping cipher: %w", err)
89+
}
90+
var masked [16]byte
91+
for i := range masked {
92+
masked[i] = rawKey[i] ^ ekey[16+i]
93+
}
94+
cipher.Encrypt(ekey[56:72], masked[:])
95+
96+
var senderSAP [128]byte
97+
decryptFairPlayMessage(m3, senderSAP[:])
98+
macKey := fpsapDescriptorForSAP(senderSAP, receiverSAP)
99+
mac := hmac.New(sha1.New, macKey[:])
100+
_, _ = mac.Write(ekey[:36])
101+
_, _ = mac.Write(rawKey[:])
102+
copy(ekey[36:56], mac.Sum(nil))
103+
return ekey, nil
75104
}

0 commit comments

Comments
 (0)