@@ -2,7 +2,11 @@ package airplay
22
33import (
44 "crypto/aes"
5+ "crypto/hmac"
6+ "crypto/sha1"
57 "encoding/binary"
8+ "fmt"
9+ "io"
610)
711
812var 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